NGA Filter

NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名、提醒过滤。troll must die。

当前为 2024-08-26 提交的版本,查看 最新版本

  1. // ==UserScript==
  2.  
  3. // @name NGA Filter
  4. // @name:zh-CN NGA 屏蔽插件
  5.  
  6. // @description NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名、提醒过滤。troll must die。
  7. // @description:zh-CN NGA 屏蔽插件,支持用户、标记、关键字、属地、小号、流量号、低声望、匿名、提醒过滤。troll must die。
  8.  
  9. // @namespace https://greasyfork.org/users/263018
  10. // @version 2.6.0
  11. // @author snyssss
  12. // @license MIT
  13.  
  14. // @match *://bbs.nga.cn/*
  15. // @match *://ngabbs.com/*
  16. // @match *://nga.178.com/*
  17.  
  18. // @require https://update.greasyfork.org/scripts/486070/1416483/NGA%20Library.js
  19.  
  20. // @grant GM_setValue
  21. // @grant GM_getValue
  22. // @grant GM_registerMenuCommand
  23. // @grant unsafeWindow
  24.  
  25. // @run-at document-start
  26. // @noframes
  27.  
  28. // ==/UserScript==
  29.  
  30. (() => {
  31. // 声明泥潭主模块、主题模块、回复模块、提醒模块
  32. let commonui, topicModule, replyModule, notificationModule;
  33.  
  34. // KEY
  35. const DATA_KEY = "NGAFilter";
  36. const PRE_FILTER_KEY = "PRE_FILTER_KEY";
  37. const NOTIFICATION_FILTER_KEY = "NOTIFICATION_FILTER_KEY";
  38.  
  39. // TIPS
  40. const TIPS = {
  41. filterMode:
  42. "过滤顺序:用户 &gt; 标记 &gt; 关键字 &gt; 属地<br/>过滤级别:显示 &gt; 隐藏 &gt; 遮罩 &gt; 标记 &gt; 继承",
  43. addTags: `一次性添加多个标记用"|"隔开,不会添加重名标记`,
  44. keyword: `支持正则表达式。比如同类型的可以写在一条规则内用"|"隔开,"ABC|DEF"即为屏蔽带有ABC或者DEF的内容。`,
  45. forumOrSubset:
  46. "输入版面或合集的完整链接,如:<br/>https://bbs.nga.cn/thread.php?fid=xxx<br/>https://bbs.nga.cn/thread.php?stid=xxx",
  47. hunter: "猎巫模块需要占用额外的资源,请谨慎开启",
  48. filterNotification: "目前支持过滤回复提醒,暂不支持过滤短消息。",
  49. filterTopicPerPage:
  50. "泥潭每页帖子数量约为30条,本功能即为检测连续的30条帖子内相同用户的发帖数量",
  51. error: "目前泥潭对查询接口增加了限制,功能基本失效",
  52. };
  53.  
  54. // 主题
  55. const THEMES = {
  56. system: {
  57. name: "系统",
  58. },
  59. classic: {
  60. name: "经典",
  61. fontColor: "crimson",
  62. borderColor: "#66BAB7",
  63. backgroundColor: "#81C7D4",
  64. },
  65. };
  66.  
  67. /**
  68. * 设置
  69. *
  70. * 暂时整体处理模块设置,后续再拆分
  71. */
  72. class Settings {
  73. /**
  74. * 缓存管理
  75. */
  76. cache;
  77.  
  78. /**
  79. * 当前设置
  80. */
  81. data = null;
  82.  
  83. /**
  84. * 初始化并绑定缓存管理
  85. * @param {Cache} cache 缓存管理
  86. */
  87. constructor(cache) {
  88. this.cache = cache;
  89. }
  90.  
  91. /**
  92. * 读取设置
  93. */
  94. async load() {
  95. // 读取设置
  96. if (this.data === null) {
  97. // 默认配置
  98. const defaultData = {
  99. tags: {},
  100. users: {},
  101. keywords: {},
  102. locations: {},
  103. forumOrSubsets: {},
  104. options: {
  105. theme: "system",
  106. filterRegdateLimit: 0,
  107. filterPostnumLimit: 0,
  108. filterTopicRateLimit: 100,
  109. filterTopicPerPageLimit: NaN,
  110. filterReputationLimit: NaN,
  111. filterAnony: false,
  112. filterThumb: false,
  113. filterMode: "隐藏",
  114. },
  115. };
  116.  
  117. // 读取数据
  118. const storedData = await this.cache
  119. .get(DATA_KEY)
  120. .then((values) => values || {});
  121.  
  122. // 写入缓存
  123. this.data = Tools.merge({}, defaultData, storedData);
  124.  
  125. // 写入默认模块选项
  126. if (Object.hasOwn(this.data, "modules") === false) {
  127. this.data.modules = ["user", "tag", "misc"];
  128.  
  129. if (Object.keys(this.data.keywords).length > 0) {
  130. this.data.modules.push("keyword");
  131. }
  132.  
  133. if (Object.keys(this.data.locations).length > 0) {
  134. this.data.modules.push("location");
  135. }
  136. }
  137. }
  138.  
  139. // 返回设置
  140. return this.data;
  141. }
  142.  
  143. /**
  144. * 写入设置
  145. */
  146. async save() {
  147. return this.cache.put(DATA_KEY, this.data);
  148. }
  149.  
  150. /**
  151. * 获取模块列表
  152. */
  153. get modules() {
  154. return this.data.modules;
  155. }
  156.  
  157. /**
  158. * 设置模块列表
  159. */
  160. set modules(values) {
  161. this.data.modules = values;
  162. this.save();
  163. }
  164.  
  165. /**
  166. * 获取标签列表
  167. */
  168. get tags() {
  169. return this.data.tags;
  170. }
  171.  
  172. /**
  173. * 设置标签列表
  174. */
  175. set tags(values) {
  176. this.data.tags = values;
  177. this.save();
  178. }
  179.  
  180. /**
  181. * 获取用户列表
  182. */
  183. get users() {
  184. return this.data.users;
  185. }
  186.  
  187. /**
  188. * 设置用户列表
  189. */
  190. set users(values) {
  191. this.data.users = values;
  192. this.save();
  193. }
  194.  
  195. /**
  196. * 获取关键字列表
  197. */
  198. get keywords() {
  199. return this.data.keywords;
  200. }
  201.  
  202. /**
  203. * 设置关键字列表
  204. */
  205. set keywords(values) {
  206. this.data.keywords = values;
  207. this.save();
  208. }
  209.  
  210. /**
  211. * 获取属地列表
  212. */
  213. get locations() {
  214. return this.data.locations;
  215. }
  216.  
  217. /**
  218. * 设置属地列表
  219. */
  220. set locations(values) {
  221. this.data.locations = values;
  222. this.save();
  223. }
  224.  
  225. /**
  226. * 获取版面或合集列表
  227. */
  228. get forumOrSubsets() {
  229. return this.data.forumOrSubsets;
  230. }
  231.  
  232. /**
  233. * 设置版面或合集列表
  234. */
  235. set forumOrSubsets(values) {
  236. this.data.forumOrSubsets = values;
  237. this.save();
  238. }
  239.  
  240. /**
  241. * 获取默认过滤模式
  242. */
  243. get defaultFilterMode() {
  244. return this.data.options.filterMode;
  245. }
  246.  
  247. /**
  248. * 设置默认过滤模式
  249. */
  250. set defaultFilterMode(value) {
  251. this.data.options.filterMode = value;
  252. this.save();
  253. }
  254.  
  255. /**
  256. * 获取主题
  257. */
  258. get theme() {
  259. return this.data.options.theme;
  260. }
  261.  
  262. /**
  263. * 设置主题
  264. */
  265. set theme(value) {
  266. this.data.options.theme = value;
  267. this.save();
  268. }
  269.  
  270. /**
  271. * 获取注册时间限制
  272. */
  273. get filterRegdateLimit() {
  274. return this.data.options.filterRegdateLimit || 0;
  275. }
  276.  
  277. /**
  278. * 设置注册时间限制
  279. */
  280. set filterRegdateLimit(value) {
  281. this.data.options.filterRegdateLimit = value;
  282. this.save();
  283. }
  284.  
  285. /**
  286. * 获取发帖数量限制
  287. */
  288. get filterPostnumLimit() {
  289. return this.data.options.filterPostnumLimit || 0;
  290. }
  291.  
  292. /**
  293. * 设置发帖数量限制
  294. */
  295. set filterPostnumLimit(value) {
  296. this.data.options.filterPostnumLimit = value;
  297. this.save();
  298. }
  299.  
  300. /**
  301. * 获取发帖比例限制
  302. */
  303. get filterTopicRateLimit() {
  304. return this.data.options.filterTopicRateLimit || 100;
  305. }
  306.  
  307. /**
  308. * 设置发帖比例限制
  309. */
  310. set filterTopicRateLimit(value) {
  311. this.data.options.filterTopicRateLimit = value;
  312. this.save();
  313. }
  314.  
  315. /**
  316. * 获取每页主题数量限制
  317. */
  318. get filterTopicPerPageLimit() {
  319. return this.data.options.filterTopicPerPageLimit || NaN;
  320. }
  321.  
  322. /**
  323. * 设置每页主题数量限制
  324. */
  325. set filterTopicPerPageLimit(value) {
  326. this.data.options.filterTopicPerPageLimit = value;
  327. this.save();
  328. }
  329.  
  330. /**
  331. * 获取版面声望限制
  332. */
  333. get filterReputationLimit() {
  334. return this.data.options.filterReputationLimit || NaN;
  335. }
  336.  
  337. /**
  338. * 设置版面声望限制
  339. */
  340. set filterReputationLimit(value) {
  341. this.data.options.filterReputationLimit = value;
  342. this.save();
  343. }
  344.  
  345. /**
  346. * 获取是否过滤匿名
  347. */
  348. get filterAnonymous() {
  349. return this.data.options.filterAnony || false;
  350. }
  351.  
  352. /**
  353. * 设置是否过滤匿名
  354. */
  355. set filterAnonymous(value) {
  356. this.data.options.filterAnony = value;
  357. this.save();
  358. }
  359.  
  360. /**
  361. * 获取是否过滤缩略图
  362. */
  363. get filterThumbnail() {
  364. return this.data.options.filterThumb || false;
  365. }
  366.  
  367. /**
  368. * 设置是否过滤缩略图
  369. */
  370. set filterThumbnail(value) {
  371. this.data.options.filterThumb = value;
  372. this.save();
  373. }
  374.  
  375. /**
  376. * 获取是否启用前置过滤
  377. */
  378. get preFilterEnabled() {
  379. return this.cache.get(PRE_FILTER_KEY).then((value) => {
  380. if (value === undefined) {
  381. return true;
  382. }
  383.  
  384. return value;
  385. });
  386. }
  387.  
  388. /**
  389. * 设置是否启用前置过滤
  390. */
  391. set preFilterEnabled(value) {
  392. this.cache.put(PRE_FILTER_KEY, value).then(() => {
  393. location.reload();
  394. });
  395. }
  396.  
  397. /**
  398. * 获取是否启用提醒过滤
  399. */
  400. get notificationFilterEnabled() {
  401. return this.cache.get(NOTIFICATION_FILTER_KEY).then((value) => {
  402. if (value === undefined) {
  403. return false;
  404. }
  405.  
  406. return value;
  407. });
  408. }
  409.  
  410. /**
  411. * 设置是否启用提醒过滤
  412. */
  413. set notificationFilterEnabled(value) {
  414. this.cache.put(NOTIFICATION_FILTER_KEY, value).then(() => {
  415. location.reload();
  416. });
  417. }
  418.  
  419. /**
  420. * 获取过滤模式列表
  421. *
  422. * 模拟成从配置中获取
  423. */
  424. get filterModes() {
  425. return ["继承", "标记", "遮罩", "隐藏", "显示"];
  426. }
  427.  
  428. /**
  429. * 获取指定下标过滤模式
  430. * @param {Number} index 下标
  431. */
  432. getNameByMode(index) {
  433. const modes = this.filterModes;
  434.  
  435. return modes[index] || "";
  436. }
  437.  
  438. /**
  439. * 获取指定过滤模式下标
  440. * @param {String} name 过滤模式
  441. */
  442. getModeByName(name) {
  443. const modes = this.filterModes;
  444.  
  445. return modes.indexOf(name);
  446. }
  447.  
  448. /**
  449. * 切换过滤模式
  450. * @param {String} value 过滤模式
  451. * @returns {String} 过滤模式
  452. */
  453. switchModeByName(value) {
  454. const index = this.getModeByName(value);
  455.  
  456. const nextIndex = (index + 1) % this.filterModes.length;
  457.  
  458. return this.filterModes[nextIndex];
  459. }
  460. }
  461.  
  462. /**
  463. * UI
  464. */
  465. class UI {
  466. /**
  467. * 标签
  468. */
  469. static label = "屏蔽";
  470.  
  471. /**
  472. * 设置
  473. */
  474. settings;
  475.  
  476. /**
  477. * API
  478. */
  479. api;
  480.  
  481. /**
  482. * 模块列表
  483. */
  484. modules = {};
  485.  
  486. /**
  487. * 菜单元素
  488. */
  489. menu = null;
  490.  
  491. /**
  492. * 视图元素
  493. */
  494. views = {};
  495.  
  496. /**
  497. * 初始化并绑定设置、API,注册脚本菜单
  498. * @param {Settings} settings 设置
  499. * @param {API} api API
  500. */
  501. constructor(settings, api) {
  502. this.settings = settings;
  503. this.api = api;
  504.  
  505. this.init();
  506. }
  507.  
  508. /**
  509. * 初始化,创建基础视图,初始化通用设置
  510. */
  511. init() {
  512. const tabs = this.createTabs({
  513. className: "right_",
  514. });
  515.  
  516. const content = this.createElement("DIV", [], {
  517. style: "width: 80vw;",
  518. });
  519.  
  520. const container = this.createElement("DIV", [tabs, content]);
  521.  
  522. this.views = {
  523. tabs,
  524. content,
  525. container,
  526. };
  527.  
  528. this.initSettings();
  529. }
  530.  
  531. /**
  532. * 初始化设置
  533. */
  534. initSettings() {
  535. // 创建基础视图
  536. const settings = this.createElement("DIV", []);
  537.  
  538. // 添加设置项
  539. const add = (order, ...elements) => {
  540. const items = [...settings.childNodes];
  541.  
  542. if (items.find((item) => item.order === order)) {
  543. return;
  544. }
  545.  
  546. const item = this.createElement(
  547. "DIV",
  548. [...elements, this.createElement("BR", [])],
  549. {
  550. order,
  551. }
  552. );
  553.  
  554. const anchor = items.find((item) => item.order > order);
  555.  
  556. settings.insertBefore(item, anchor || null);
  557.  
  558. return item;
  559. };
  560.  
  561. // 绑定事件
  562. Object.assign(settings, {
  563. add,
  564. });
  565.  
  566. // 合并视图
  567. Object.assign(this.views, {
  568. settings,
  569. });
  570.  
  571. // 创建标签页
  572. const { tabs, content } = this.views;
  573.  
  574. this.createTab(tabs, "设置", Number.MAX_SAFE_INTEGER, {
  575. onclick: () => {
  576. content.innerHTML = "";
  577. content.appendChild(settings);
  578. },
  579. });
  580. }
  581.  
  582. /**
  583. * 弹窗确认
  584. * @param {String} message 提示信息
  585. * @returns {Promise}
  586. */
  587. confirm(message = "是否确认?") {
  588. return new Promise((resolve, reject) => {
  589. const result = confirm(message);
  590.  
  591. if (result) {
  592. resolve();
  593. return;
  594. }
  595.  
  596. reject();
  597. });
  598. }
  599.  
  600. /**
  601. * 折叠
  602. * @param {String | Number} key 标识
  603. * @param {HTMLElement} element 目标元素
  604. * @param {String} content 内容
  605. */
  606. collapse(key, element, content) {
  607. key = "collapsed_" + key;
  608.  
  609. element.innerHTML = `
  610. <div class="lessernuke filter-mask-collapse" onclick="[...document.getElementsByName('${key}')].forEach(item => item.style.display = '')">
  611. <span class="filter-mask-hint">Troll must die.</span>
  612. <div style="display: none;" name="${key}">
  613. ${content}
  614. </div>
  615. </div>`;
  616. }
  617.  
  618. /**
  619. * 创建元素
  620. * @param {String} tagName 标签
  621. * @param {HTMLElement | HTMLElement[] | String} content 内容,元素或者 innerHTML
  622. * @param {*} properties 额外属性
  623. * @returns {HTMLElement} 元素
  624. */
  625. createElement(tagName, content, properties = {}) {
  626. const element = document.createElement(tagName);
  627.  
  628. // 写入内容
  629. if (typeof content === "string") {
  630. element.innerHTML = content;
  631. } else {
  632. if (Array.isArray(content) === false) {
  633. content = [content];
  634. }
  635.  
  636. content.forEach((item) => {
  637. if (item === null) {
  638. return;
  639. }
  640.  
  641. if (typeof item === "string") {
  642. element.append(item);
  643. return;
  644. }
  645.  
  646. element.appendChild(item);
  647. });
  648. }
  649.  
  650. // 对 A 标签的额外处理
  651. if (tagName.toUpperCase() === "A") {
  652. if (Object.hasOwn(properties, "href") === false) {
  653. properties.href = "javascript: void(0);";
  654. }
  655. }
  656.  
  657. // 附加属性
  658. Object.entries(properties).forEach(([key, value]) => {
  659. element[key] = value;
  660. });
  661.  
  662. return element;
  663. }
  664.  
  665. /**
  666. * 创建按钮
  667. * @param {String} text 文字
  668. * @param {Function} onclick 点击事件
  669. * @param {*} properties 额外属性
  670. */
  671. createButton(text, onclick, properties = {}) {
  672. return this.createElement("BUTTON", text, {
  673. ...properties,
  674. onclick,
  675. });
  676. }
  677.  
  678. /**
  679. * 创建按钮组
  680. * @param {Array} buttons 按钮集合
  681. */
  682. createButtonGroup(...buttons) {
  683. return this.createElement("DIV", buttons, {
  684. className: "filter-button-group",
  685. });
  686. }
  687.  
  688. /**
  689. * 创建表格
  690. * @param {Array} headers 表头集合
  691. * @param {*} properties 额外属性
  692. * @returns {HTMLElement} 元素和相关函数
  693. */
  694. createTable(headers, properties = {}) {
  695. const rows = [];
  696.  
  697. const ths = headers.map((item, index) =>
  698. this.createElement("TH", item.label, {
  699. ...item,
  700. className: `c${index + 1}`,
  701. })
  702. );
  703.  
  704. const tr =
  705. ths.length > 0
  706. ? this.createElement("TR", ths, {
  707. className: "block_txt_c0",
  708. })
  709. : null;
  710.  
  711. const thead = tr !== null ? this.createElement("THEAD", tr) : null;
  712.  
  713. const tbody = this.createElement("TBODY", []);
  714.  
  715. const table = this.createElement("TABLE", [thead, tbody], {
  716. ...properties,
  717. className: "filter-table forumbox",
  718. });
  719.  
  720. const wrapper = this.createElement("DIV", table, {
  721. className: "filter-table-wrapper",
  722. });
  723.  
  724. const intersectionObserver = new IntersectionObserver((entries) => {
  725. if (entries[0].intersectionRatio <= 0) return;
  726.  
  727. const list = rows.splice(0, 10);
  728.  
  729. if (list.length === 0) {
  730. return;
  731. }
  732.  
  733. intersectionObserver.disconnect();
  734.  
  735. tbody.append(...list);
  736.  
  737. intersectionObserver.observe(tbody.lastElementChild);
  738. });
  739.  
  740. const add = (...columns) => {
  741. const tds = columns.map((column, index) => {
  742. if (ths[index]) {
  743. const { center, ellipsis } = ths[index];
  744.  
  745. const properties = {};
  746.  
  747. if (center) {
  748. properties.style = "text-align: center;";
  749. }
  750.  
  751. if (ellipsis) {
  752. properties.className = "filter-text-ellipsis";
  753. }
  754.  
  755. column = this.createElement("DIV", column, properties);
  756. }
  757.  
  758. return this.createElement("TD", column, {
  759. className: `c${index + 1}`,
  760. });
  761. });
  762.  
  763. const tr = this.createElement("TR", tds, {
  764. className: `row${(rows.length % 2) + 1}`,
  765. });
  766.  
  767. intersectionObserver.disconnect();
  768.  
  769. rows.push(tr);
  770.  
  771. intersectionObserver.observe(tbody.lastElementChild || tbody);
  772. };
  773.  
  774. const update = (e, ...columns) => {
  775. const row = e.target.closest("TR");
  776.  
  777. if (row) {
  778. const tds = row.querySelectorAll("TD");
  779.  
  780. columns.map((column, index) => {
  781. if (ths[index]) {
  782. const { center, ellipsis } = ths[index];
  783.  
  784. const properties = {};
  785.  
  786. if (center) {
  787. properties.style = "text-align: center;";
  788. }
  789.  
  790. if (ellipsis) {
  791. properties.className = "filter-text-ellipsis";
  792. }
  793.  
  794. column = this.createElement("DIV", column, properties);
  795. }
  796.  
  797. if (tds[index]) {
  798. tds[index].innerHTML = "";
  799. tds[index].append(column);
  800. }
  801. });
  802. }
  803. };
  804.  
  805. const remove = (e) => {
  806. const row = e.target.closest("TR");
  807.  
  808. if (row) {
  809. tbody.removeChild(row);
  810. }
  811. };
  812.  
  813. const clear = () => {
  814. rows.splice(0);
  815. intersectionObserver.disconnect();
  816.  
  817. tbody.innerHTML = "";
  818. };
  819.  
  820. Object.assign(wrapper, {
  821. add,
  822. update,
  823. remove,
  824. clear,
  825. });
  826.  
  827. return wrapper;
  828. }
  829.  
  830. /**
  831. * 创建标签组
  832. * @param {*} properties 额外属性
  833. */
  834. createTabs(properties = {}) {
  835. const tabs = this.createElement(
  836. "DIV",
  837. `<table class="stdbtn" cellspacing="0">
  838. <tbody>
  839. <tr></tr>
  840. </tbody>
  841. </table>`,
  842. properties
  843. );
  844.  
  845. return this.createElement(
  846. "DIV",
  847. [
  848. tabs,
  849. this.createElement("DIV", [], {
  850. className: "clear",
  851. }),
  852. ],
  853. {
  854. style: "display: none; margin-bottom: 5px;",
  855. }
  856. );
  857. }
  858.  
  859. /**
  860. * 创建标签
  861. * @param {Element} tabs 标签组
  862. * @param {String} label 标签名称
  863. * @param {Number} order 标签顺序,重复则跳过
  864. * @param {*} properties 额外属性
  865. */
  866. createTab(tabs, label, order, properties = {}) {
  867. const group = tabs.querySelector("TR");
  868.  
  869. const items = [...group.childNodes];
  870.  
  871. if (items.find((item) => item.order === order)) {
  872. return;
  873. }
  874.  
  875. if (items.length > 0) {
  876. tabs.style.removeProperty("display");
  877. }
  878.  
  879. const tab = this.createElement("A", label, {
  880. ...properties,
  881. className: "nobr silver",
  882. onclick: () => {
  883. if (tab.className === "nobr") {
  884. return;
  885. }
  886.  
  887. group.querySelectorAll("A").forEach((item) => {
  888. if (item === tab) {
  889. item.className = "nobr";
  890. } else {
  891. item.className = "nobr silver";
  892. }
  893. });
  894.  
  895. if (properties.onclick) {
  896. properties.onclick();
  897. }
  898. },
  899. });
  900.  
  901. const wrapper = this.createElement("TD", tab, {
  902. order,
  903. });
  904.  
  905. const anchor = items.find((item) => item.order > order);
  906.  
  907. group.insertBefore(wrapper, anchor || null);
  908.  
  909. return wrapper;
  910. }
  911.  
  912. /**
  913. * 创建对话框
  914. * @param {HTMLElement | null} anchor 要绑定的元素,如果为空,直接弹出
  915. * @param {String} title 对话框的标题
  916. * @param {HTMLElement} content 对话框的内容
  917. */
  918. createDialog(anchor, title, content) {
  919. let window;
  920.  
  921. const show = () => {
  922. if (window === undefined) {
  923. window = commonui.createCommmonWindow();
  924. }
  925.  
  926. window._.addContent(null);
  927. window._.addTitle(title);
  928. window._.addContent(content);
  929. window._.show();
  930. };
  931.  
  932. if (anchor) {
  933. anchor.onclick = show;
  934. } else {
  935. show();
  936. }
  937.  
  938. return window;
  939. }
  940.  
  941. /**
  942. * 渲染菜单
  943. */
  944. renderMenu() {
  945. // 菜单尚未加载完成
  946. if (
  947. commonui.mainMenu === undefined ||
  948. commonui.mainMenu.dataReady === null
  949. ) {
  950. return;
  951. }
  952.  
  953. // 定位右侧菜单容器
  954. const right = document.querySelector("#mainmenu .right");
  955.  
  956. if (right === null) {
  957. return;
  958. }
  959.  
  960. // 定位搜索框,如果有搜索框,放在搜索框左侧,没有放在最后
  961. const searchInput = right.querySelector("#menusearchinput");
  962.  
  963. // 初始化菜单并绑定
  964. if (this.menu === null) {
  965. const menu = this.createElement("A", this.constructor.label, {
  966. className: "mmdefault nobr",
  967. });
  968.  
  969. this.menu = menu;
  970. }
  971.  
  972. // 插入菜单
  973. const container = this.createElement("DIV", this.menu, {
  974. className: "td",
  975. });
  976.  
  977. if (searchInput) {
  978. searchInput.closest("DIV").before(container);
  979. } else {
  980. right.appendChild(container);
  981. }
  982. }
  983.  
  984. /**
  985. * 渲染视图
  986. */
  987. renderView() {
  988. // 如果菜单还没有渲染,说明模块尚未加载完毕,跳过
  989. if (this.menu === null) {
  990. return;
  991. }
  992.  
  993. // 绑定菜单点击事件.
  994. this.createDialog(
  995. this.menu,
  996. this.constructor.label,
  997. this.views.container
  998. );
  999.  
  1000. // 启用第一个模块
  1001. this.views.tabs.querySelector("A").click();
  1002. }
  1003.  
  1004. /**
  1005. * 渲染
  1006. */
  1007. render() {
  1008. this.renderMenu();
  1009. this.renderView();
  1010. }
  1011. }
  1012.  
  1013. /**
  1014. * 基础模块
  1015. */
  1016. class Module {
  1017. /**
  1018. * 模块名称
  1019. */
  1020. static name;
  1021.  
  1022. /**
  1023. * 模块标签
  1024. */
  1025. static label;
  1026.  
  1027. /**
  1028. * 顺序
  1029. */
  1030. static order;
  1031.  
  1032. /**
  1033. * 依赖模块
  1034. */
  1035. static depends = [];
  1036.  
  1037. /**
  1038. * 附加模块
  1039. */
  1040. static addons = [];
  1041.  
  1042. /**
  1043. * 设置
  1044. */
  1045. settings;
  1046.  
  1047. /**
  1048. * API
  1049. */
  1050. api;
  1051.  
  1052. /**
  1053. * UI
  1054. */
  1055. ui;
  1056.  
  1057. /**
  1058. * 过滤列表
  1059. */
  1060. data = [];
  1061.  
  1062. /**
  1063. * 依赖模块
  1064. */
  1065. depends = {};
  1066.  
  1067. /**
  1068. * 附加模块
  1069. */
  1070. addons = {};
  1071.  
  1072. /**
  1073. * 视图元素
  1074. */
  1075. views = {};
  1076.  
  1077. /**
  1078. * 初始化并绑定设置、API、UI、过滤列表,注册 UI
  1079. * @param {Settings} settings 设置
  1080. * @param {API} api API
  1081. * @param {UI} ui UI
  1082. */
  1083. constructor(settings, api, ui, data) {
  1084. this.settings = settings;
  1085. this.api = api;
  1086. this.ui = ui;
  1087.  
  1088. this.data = data;
  1089.  
  1090. this.init();
  1091. }
  1092.  
  1093. /**
  1094. * 创建实例
  1095. * @param {Settings} settings 设置
  1096. * @param {API} api API
  1097. * @param {UI} ui UI
  1098. * @param {Array} data 过滤列表
  1099. * @returns {Module | null} 成功后返回模块实例
  1100. */
  1101. static create(settings, api, ui, data) {
  1102. // 读取设置里的模块列表
  1103. const modules = settings.modules;
  1104.  
  1105. // 如果不包含自己或依赖的模块,则返回空
  1106. const index = [this, ...this.depends].findIndex(
  1107. (module) => modules.includes(module.name) === false
  1108. );
  1109.  
  1110. if (index >= 0) {
  1111. return null;
  1112. }
  1113.  
  1114. // 创建实例
  1115. const instance = new this(settings, api, ui, data);
  1116.  
  1117. // 返回实例
  1118. return instance;
  1119. }
  1120.  
  1121. /**
  1122. * 判断指定附加模块是否启用
  1123. * @param {typeof Module} module 模块
  1124. */
  1125. hasAddon(module) {
  1126. return Object.hasOwn(this.addons, module.name);
  1127. }
  1128.  
  1129. /**
  1130. * 初始化,创建基础视图和组件
  1131. */
  1132. init() {
  1133. if (this.views.container) {
  1134. this.destroy();
  1135. }
  1136.  
  1137. const { ui } = this;
  1138.  
  1139. const container = ui.createElement("DIV", []);
  1140.  
  1141. this.views = {
  1142. container,
  1143. };
  1144.  
  1145. this.initComponents();
  1146. }
  1147.  
  1148. /**
  1149. * 初始化组件
  1150. */
  1151. initComponents() {}
  1152.  
  1153. /**
  1154. * 销毁
  1155. */
  1156. destroy() {
  1157. Object.values(this.views).forEach((view) => {
  1158. if (view.parentNode) {
  1159. view.parentNode.removeChild(view);
  1160. }
  1161. });
  1162.  
  1163. this.views = {};
  1164. }
  1165.  
  1166. /**
  1167. * 渲染
  1168. * @param {HTMLElement} container 容器
  1169. */
  1170. render(container) {
  1171. container.innerHTML = "";
  1172. container.appendChild(this.views.container);
  1173. }
  1174.  
  1175. /**
  1176. * 过滤
  1177. * @param {*} item 绑定的 nFilter
  1178. * @param {*} result 过滤结果
  1179. */
  1180. async filter(item, result) {}
  1181.  
  1182. /**
  1183. * 通知
  1184. * @param {*} item 绑定的 nFilter
  1185. * @param {*} result 过滤结果
  1186. */
  1187. async notify(item, result) {}
  1188. }
  1189.  
  1190. /**
  1191. * 过滤器
  1192. */
  1193. class Filter {
  1194. /**
  1195. * 设置
  1196. */
  1197. settings;
  1198.  
  1199. /**
  1200. * API
  1201. */
  1202. api;
  1203.  
  1204. /**
  1205. * UI
  1206. */
  1207. ui;
  1208.  
  1209. /**
  1210. * 过滤列表
  1211. */
  1212. data = [];
  1213.  
  1214. /**
  1215. * 模块列表
  1216. */
  1217. modules = {};
  1218.  
  1219. /**
  1220. * 初始化并绑定设置、API、UI
  1221. * @param {Settings} settings 设置
  1222. * @param {API} api API
  1223. * @param {UI} ui UI
  1224. */
  1225. constructor(settings, api, ui) {
  1226. this.settings = settings;
  1227. this.api = api;
  1228. this.ui = ui;
  1229. }
  1230.  
  1231. /**
  1232. * 绑定两个模块的互相关系
  1233. * @param {Module} moduleA 模块A
  1234. * @param {Module} moduleB 模块B
  1235. */
  1236. bindModule(moduleA, moduleB) {
  1237. const nameA = moduleA.constructor.name;
  1238. const nameB = moduleB.constructor.name;
  1239.  
  1240. // A 依赖 B
  1241. if (moduleA.constructor.depends.findIndex((i) => i.name === nameB) >= 0) {
  1242. moduleA.depends[nameB] = moduleB;
  1243. moduleA.init();
  1244. }
  1245.  
  1246. // B 依赖 A
  1247. if (moduleB.constructor.depends.findIndex((i) => i.name === nameA) >= 0) {
  1248. moduleB.depends[nameA] = moduleA;
  1249. moduleB.init();
  1250. }
  1251.  
  1252. // A 附加 B
  1253. if (moduleA.constructor.addons.findIndex((i) => i.name === nameB) >= 0) {
  1254. moduleA.addons[nameB] = moduleB;
  1255. moduleA.init();
  1256. }
  1257.  
  1258. // B 附加 A
  1259. if (moduleB.constructor.addons.findIndex((i) => i.name === nameA) >= 0) {
  1260. moduleB.addons[nameA] = moduleA;
  1261. moduleB.init();
  1262. }
  1263. }
  1264.  
  1265. /**
  1266. * 加载模块
  1267. * @param {typeof Module} module 模块
  1268. */
  1269. initModule(module) {
  1270. // 如果已经加载过则跳过
  1271. if (Object.hasOwn(this.modules, module.name)) {
  1272. return;
  1273. }
  1274.  
  1275. // 创建模块
  1276. const instance = module.create(
  1277. this.settings,
  1278. this.api,
  1279. this.ui,
  1280. this.data
  1281. );
  1282.  
  1283. // 如果创建失败则跳过
  1284. if (instance === null) {
  1285. return;
  1286. }
  1287.  
  1288. // 绑定依赖模块和附加模块
  1289. Object.values(this.modules).forEach((item) => {
  1290. this.bindModule(item, instance);
  1291. });
  1292.  
  1293. // 合并模块
  1294. this.modules[module.name] = instance;
  1295.  
  1296. // 按照顺序重新整理模块
  1297. this.modules = Tools.sortBy(
  1298. Object.values(this.modules),
  1299. (item) => item.constructor.order
  1300. ).reduce(
  1301. (result, item) => ({
  1302. ...result,
  1303. [item.constructor.name]: item,
  1304. }),
  1305. {}
  1306. );
  1307. }
  1308.  
  1309. /**
  1310. * 加载模块列表
  1311. * @param {typeof Module[]} modules 模块列表
  1312. */
  1313. initModules(...modules) {
  1314. // 根据依赖和附加模块决定初始化的顺序
  1315. Tools.sortBy(
  1316. modules,
  1317. (item) => item.depends.length,
  1318. (item) => item.addons.length
  1319. ).forEach((module) => {
  1320. this.initModule(module);
  1321. });
  1322. }
  1323.  
  1324. /**
  1325. * 添加到过滤列表
  1326. * @param {*} item 绑定的 nFilter
  1327. */
  1328. pushData(item) {
  1329. // 清除掉无效数据
  1330. for (let i = 0; i < this.data.length; ) {
  1331. if (document.body.contains(this.data[i].container) === false) {
  1332. this.data.splice(i, 1);
  1333. continue;
  1334. }
  1335.  
  1336. i += 1;
  1337. }
  1338.  
  1339. // 加入过滤列表
  1340. if (this.data.includes(item) === false) {
  1341. this.data.push(item);
  1342. }
  1343. }
  1344.  
  1345. /**
  1346. * 判断指定 UID 是否是自己
  1347. * @param {Number} uid 用户 ID
  1348. */
  1349. isSelf(uid) {
  1350. return unsafeWindow.__CURRENT_UID === uid;
  1351. }
  1352.  
  1353. /**
  1354. * 获取过滤模式
  1355. * @param {*} item 绑定的 nFilter
  1356. */
  1357. async getFilterMode(item) {
  1358. // 获取链接参数
  1359. const params = new URLSearchParams(location.search);
  1360.  
  1361. // 跳过屏蔽(插件自定义)
  1362. if (params.has("nofilter")) {
  1363. return;
  1364. }
  1365.  
  1366. // 收藏
  1367. if (params.has("favor")) {
  1368. return;
  1369. }
  1370.  
  1371. // 只看某人
  1372. if (params.has("authorid")) {
  1373. return;
  1374. }
  1375.  
  1376. // 跳过自己
  1377. if (this.isSelf(item.uid)) {
  1378. return;
  1379. }
  1380.  
  1381. // 声明结果
  1382. const result = {
  1383. mode: -1,
  1384. reason: ``,
  1385. };
  1386.  
  1387. // 根据模块依次过滤
  1388. for (const module of Object.values(this.modules)) {
  1389. await module.filter(item, result);
  1390. }
  1391.  
  1392. // 写入过滤模式和过滤原因
  1393. item.filterMode = this.settings.getNameByMode(result.mode);
  1394. item.reason = result.reason;
  1395.  
  1396. // 通知各模块过滤结果
  1397. for (const module of Object.values(this.modules)) {
  1398. await module.notify(item, result);
  1399. }
  1400.  
  1401. // 继承模式下返回默认过滤模式
  1402. if (item.filterMode === "继承") {
  1403. return this.settings.defaultFilterMode;
  1404. }
  1405.  
  1406. // 返回结果
  1407. return item.filterMode;
  1408. }
  1409.  
  1410. /**
  1411. * 过滤主题
  1412. * @param {*} item 主题内容,见 commonui.topicArg.data
  1413. */
  1414. filterTopic(item) {
  1415. // 绑定事件
  1416. if (item.nFilter === undefined) {
  1417. // 主题 ID
  1418. const tid = item[8];
  1419.  
  1420. // 主题版面 ID
  1421. const fid = item[7];
  1422.  
  1423. // 主题标题
  1424. const title = item[1];
  1425. const subject = title.innerText;
  1426.  
  1427. // 主题作者
  1428. const author = item[2];
  1429. const uid =
  1430. parseInt(author.getAttribute("href").match(/uid=(\S+)/)[1], 10) || 0;
  1431. const username = author.innerText;
  1432.  
  1433. // 增加操作角标
  1434. const action = (() => {
  1435. const anchor = item[2].parentNode;
  1436.  
  1437. const element = this.ui.createElement("DIV", "", {
  1438. style: Object.entries({
  1439. position: "absolute",
  1440. right: 0,
  1441. bottom: 0,
  1442. padding: "6px",
  1443. "clip-path": "polygon(100% 0, 100% 100%, 0 100%)",
  1444. })
  1445. .map(([key, value]) => `${key}: ${value}`)
  1446. .join(";"),
  1447. });
  1448.  
  1449. anchor.style.position = "relative";
  1450. anchor.appendChild(element);
  1451.  
  1452. return element;
  1453. })();
  1454.  
  1455. // 主题杂项
  1456. const topicMisc = item[16];
  1457.  
  1458. // 主题容器
  1459. const container = title.closest("tr");
  1460.  
  1461. // 过滤函数
  1462. const execute = async () => {
  1463. // 获取过滤模式
  1464. const filterMode = await this.getFilterMode(item.nFilter);
  1465.  
  1466. // 样式处理
  1467. (() => {
  1468. // 还原样式
  1469. // TODO 应该整体采用 className 来实现
  1470. (() => {
  1471. // 标记模式
  1472. title.style.removeProperty("textDecoration");
  1473.  
  1474. // 遮罩模式
  1475. title.classList.remove("filter-mask");
  1476. author.classList.remove("filter-mask");
  1477. })();
  1478.  
  1479. // 样式处理
  1480. (() => {
  1481. // 标记模式下,主题标记会有删除线标识
  1482. if (filterMode === "标记") {
  1483. title.style.textDecoration = "line-through";
  1484. return;
  1485. }
  1486.  
  1487. // 遮罩模式下,主题和作者会有遮罩样式
  1488. if (filterMode === "遮罩") {
  1489. title.classList.add("filter-mask");
  1490. author.classList.add("filter-mask");
  1491. return;
  1492. }
  1493.  
  1494. // 隐藏模式下,容器会被隐藏
  1495. if (filterMode === "隐藏") {
  1496. container.style.display = "none";
  1497. return;
  1498. }
  1499. })();
  1500.  
  1501. // 非隐藏模式下,恢复显示
  1502. if (filterMode !== "隐藏") {
  1503. container.style.removeProperty("display");
  1504. }
  1505. })();
  1506. };
  1507.  
  1508. // 绑定事件
  1509. item.nFilter = {
  1510. tid,
  1511. pid: 0,
  1512. uid,
  1513. fid,
  1514. username,
  1515. container,
  1516. title,
  1517. author,
  1518. subject,
  1519. topicMisc,
  1520. action,
  1521. tags: null,
  1522. execute,
  1523. };
  1524.  
  1525. // 添加至列表
  1526. this.pushData(item.nFilter);
  1527. }
  1528.  
  1529. // 开始过滤
  1530. item.nFilter.execute();
  1531. }
  1532.  
  1533. /**
  1534. * 过滤回复
  1535. * @param {*} item 回复内容,见 commonui.postArg.data
  1536. */
  1537. filterReply(item) {
  1538. // 跳过泥潭增加的额外内容
  1539. if (Tools.getType(item) !== "object") {
  1540. return;
  1541. }
  1542.  
  1543. // 绑定事件
  1544. if (item.nFilter === undefined) {
  1545. // 主题 ID
  1546. const tid = item.tid;
  1547.  
  1548. // 回复 ID
  1549. const pid = item.pid;
  1550.  
  1551. // 判断是否是楼层
  1552. const isFloor = typeof item.i === "number";
  1553.  
  1554. // 回复容器
  1555. const container = isFloor
  1556. ? item.uInfoC.closest("tr")
  1557. : item.uInfoC.closest(".comment_c");
  1558.  
  1559. // 回复标题
  1560. const title = item.subjectC;
  1561. const subject = title.innerText;
  1562.  
  1563. // 回复内容
  1564. const content = item.contentC;
  1565. const contentBak = content.innerHTML;
  1566.  
  1567. // 回复作者
  1568. const author =
  1569. container.querySelector(".posterInfoLine") || item.uInfoC;
  1570. const uid = parseInt(item.pAid, 10) || 0;
  1571. const username = author.querySelector(".author").innerText;
  1572. const avatar = author.querySelector(".avatar");
  1573.  
  1574. // 找到用户 ID,将其视为操作按钮
  1575. const action = container.querySelector('[name="uid"]');
  1576.  
  1577. // 创建一个元素,用于展示标记列表
  1578. // 贴条和高赞不显示
  1579. const tags = (() => {
  1580. if (isFloor === false) {
  1581. return null;
  1582. }
  1583.  
  1584. const element = document.createElement("div");
  1585.  
  1586. element.className = "filter-tags";
  1587.  
  1588. author.appendChild(element);
  1589.  
  1590. return element;
  1591. })();
  1592.  
  1593. // 过滤函数
  1594. const execute = async () => {
  1595. // 获取过滤模式
  1596. const filterMode = await this.getFilterMode(item.nFilter);
  1597.  
  1598. // 样式处理
  1599. (() => {
  1600. // 还原样式
  1601. // TODO 应该整体采用 className 来实现
  1602. (() => {
  1603. // 标记模式
  1604. if (avatar) {
  1605. avatar.style.removeProperty("display");
  1606. }
  1607.  
  1608. content.innerHTML = contentBak;
  1609.  
  1610. // 遮罩模式
  1611. const caption = container.parentNode.querySelector("CAPTION");
  1612.  
  1613. if (caption) {
  1614. container.parentNode.removeChild(caption);
  1615. container.style.removeProperty("display");
  1616. }
  1617. })();
  1618.  
  1619. // 样式处理
  1620. (() => {
  1621. // 标记模式下,隐藏头像,采用泥潭的折叠样式
  1622. if (filterMode === "标记") {
  1623. if (avatar) {
  1624. avatar.style.display = "none";
  1625. }
  1626.  
  1627. this.ui.collapse(uid, content, contentBak);
  1628. return;
  1629. }
  1630.  
  1631. // 遮罩模式下,楼层会有遮罩样式
  1632. if (filterMode === "遮罩") {
  1633. const caption = document.createElement("CAPTION");
  1634.  
  1635. if (isFloor) {
  1636. caption.className = "filter-mask filter-mask-block";
  1637. } else {
  1638. caption.className = "filter-mask filter-mask-block left";
  1639. caption.style.width = "47%";
  1640. }
  1641.  
  1642. caption.style.textAlign = "center";
  1643. caption.innerHTML = `<span class="filter-mask-hint">Troll must die.</span>`;
  1644. caption.onclick = () => {
  1645. const caption = container.parentNode.querySelector("CAPTION");
  1646.  
  1647. if (caption) {
  1648. container.parentNode.removeChild(caption);
  1649. container.style.removeProperty("display");
  1650. }
  1651. };
  1652.  
  1653. container.parentNode.insertBefore(caption, container);
  1654. container.style.display = "none";
  1655. return;
  1656. }
  1657.  
  1658. // 隐藏模式下,容器会被隐藏
  1659. if (filterMode === "隐藏") {
  1660. container.style.display = "none";
  1661. return;
  1662. }
  1663. })();
  1664.  
  1665. // 非隐藏模式下,恢复显示
  1666. // 楼层的遮罩模式下仍需隐藏
  1667. if (["遮罩", "隐藏"].includes(filterMode) === false) {
  1668. container.style.removeProperty("display");
  1669. }
  1670. })();
  1671.  
  1672. // 过滤引用
  1673. this.filterQuote(item);
  1674. };
  1675.  
  1676. // 绑定事件
  1677. item.nFilter = {
  1678. tid,
  1679. pid,
  1680. uid,
  1681. fid: null,
  1682. username,
  1683. container,
  1684. title,
  1685. author,
  1686. subject,
  1687. content: content.innerText,
  1688. topicMisc: "",
  1689. action,
  1690. tags,
  1691. execute,
  1692. };
  1693.  
  1694. // 添加至列表
  1695. this.pushData(item.nFilter);
  1696. }
  1697.  
  1698. // 开始过滤
  1699. item.nFilter.execute();
  1700. }
  1701.  
  1702. /**
  1703. * 过滤引用
  1704. * @param {*} item 回复内容,见 commonui.postArg.data
  1705. */
  1706. filterQuote(item) {
  1707. // 未绑定事件,直接跳过
  1708. if (item.nFilter === undefined) {
  1709. return;
  1710. }
  1711.  
  1712. // 回复内容
  1713. const content = item.contentC;
  1714.  
  1715. // 找到所有引用
  1716. const quotes = content.querySelectorAll(".quote");
  1717.  
  1718. // 处理引用
  1719. [...quotes].map(async (quote) => {
  1720. const { uid, username } = (() => {
  1721. const ele = quote.querySelector("A[href^='/nuke.php']");
  1722.  
  1723. if (ele) {
  1724. const res = ele.getAttribute("href").match(/uid=(\S+)/);
  1725.  
  1726. if (res) {
  1727. return {
  1728. uid: parseInt(res[1], 10),
  1729. username: ele.innerText.substring(1, ele.innerText.length - 1),
  1730. };
  1731. }
  1732. }
  1733.  
  1734. return {};
  1735. })();
  1736.  
  1737. const { tid, pid } = (() => {
  1738. const ele = quote.querySelector("[title='快速浏览这个帖子']");
  1739.  
  1740. if (ele) {
  1741. const res = ele
  1742. .getAttribute("onclick")
  1743. .match(/fastViewPost(.+,(\S+),(\S+|undefined),.+)/);
  1744.  
  1745. if (res) {
  1746. return {
  1747. tid: parseInt(res[2], 10),
  1748. pid: parseInt(res[3], 10) || 0,
  1749. };
  1750. }
  1751. }
  1752.  
  1753. return {};
  1754. })();
  1755.  
  1756. // 临时的 nFilter
  1757. const nFilter = {
  1758. tid,
  1759. pid,
  1760. uid,
  1761. fid: null,
  1762. username,
  1763. subject: "",
  1764. content: quote.innerText,
  1765. topicMisc: "",
  1766. action: null,
  1767. tags: null,
  1768. };
  1769.  
  1770. // 获取过滤模式
  1771. const filterMode = await this.getFilterMode(nFilter);
  1772.  
  1773. (() => {
  1774. if (filterMode === "标记") {
  1775. this.ui.collapse(uid, quote, quote.innerHTML);
  1776. return;
  1777. }
  1778.  
  1779. if (filterMode === "遮罩") {
  1780. const source = document.createElement("DIV");
  1781.  
  1782. source.innerHTML = quote.innerHTML;
  1783. source.style.display = "none";
  1784.  
  1785. const caption = document.createElement("CAPTION");
  1786.  
  1787. caption.className = "filter-mask filter-mask-block";
  1788.  
  1789. caption.style.textAlign = "center";
  1790. caption.innerHTML = `<span class="filter-mask-hint">Troll must die.</span>`;
  1791. caption.onclick = () => {
  1792. quote.removeChild(caption);
  1793.  
  1794. source.style.display = "";
  1795. };
  1796.  
  1797. quote.innerHTML = "";
  1798. quote.appendChild(source);
  1799. quote.appendChild(caption);
  1800. return;
  1801. }
  1802.  
  1803. if (filterMode === "隐藏") {
  1804. quote.innerHTML = "";
  1805. return;
  1806. }
  1807. })();
  1808.  
  1809. // 绑定引用
  1810. item.nFilter.quotes = item.nFilter.quotes || {};
  1811. item.nFilter.quotes[uid] = nFilter.filterMode;
  1812. });
  1813. }
  1814.  
  1815. /**
  1816. * 过滤提醒
  1817. * @param {*} container 提醒容器
  1818. * @param {*} item 提醒内容
  1819. */
  1820. async filterNotification(container, item) {
  1821. // 临时的 nFilter
  1822. const nFilter = {
  1823. ...item,
  1824. fid: null,
  1825. topicMisc: "",
  1826. action: null,
  1827. tags: null,
  1828. };
  1829.  
  1830. // 获取过滤模式
  1831. const filterMode = await this.getFilterMode(nFilter);
  1832.  
  1833. // 样式处理
  1834. (() => {
  1835. // 标记模式下,容器会有删除线标识
  1836. if (filterMode === "标记") {
  1837. container.style.textDecoration = "line-through";
  1838. return;
  1839. }
  1840.  
  1841. // 遮罩模式下,容器会有遮罩样式
  1842. if (filterMode === "遮罩") {
  1843. container.classList.add("filter-mask");
  1844. return;
  1845. }
  1846.  
  1847. // 隐藏模式下,容器会被隐藏
  1848. if (filterMode === "隐藏") {
  1849. container.style.display = "none";
  1850. return;
  1851. }
  1852. })();
  1853. }
  1854. }
  1855.  
  1856. /**
  1857. * 列表模块
  1858. */
  1859. class ListModule extends Module {
  1860. /**
  1861. * 模块名称
  1862. */
  1863. static name = "list";
  1864.  
  1865. /**
  1866. * 模块标签
  1867. */
  1868. static label = "列表";
  1869.  
  1870. /**
  1871. * 顺序
  1872. */
  1873. static order = 10;
  1874.  
  1875. /**
  1876. * 表格列
  1877. * @returns {Array} 表格列集合
  1878. */
  1879. columns() {
  1880. return [
  1881. { label: "内容", ellipsis: true },
  1882. { label: "过滤模式", center: true, width: 1 },
  1883. { label: "原因", width: 1 },
  1884. ];
  1885. }
  1886.  
  1887. /**
  1888. * 表格项
  1889. * @param {*} item 绑定的 nFilter
  1890. * @returns {Array} 表格项集合
  1891. */
  1892. column(item) {
  1893. const { ui } = this;
  1894. const { tid, pid, filterMode, reason } = item;
  1895.  
  1896. // 移除 BR 标签
  1897. item.content = (item.content || "").replace(/<br>/g, "");
  1898.  
  1899. // 内容
  1900. const content = (() => {
  1901. if (pid) {
  1902. return ui.createElement("A", item.content, {
  1903. href: `/read.php?pid=${pid}&nofilter`,
  1904. title: item.content,
  1905. });
  1906. }
  1907.  
  1908. // 如果有 TID 但没有标题,是引用,采用内容逻辑
  1909. if (item.subject.length === 0) {
  1910. return ui.createElement("A", item.content, {
  1911. href: `/read.php?tid=${tid}&nofilter`,
  1912. title: item.content,
  1913. });
  1914. }
  1915.  
  1916. return ui.createElement("A", item.subject, {
  1917. href: `/read.php?tid=${tid}&nofilter`,
  1918. title: item.content,
  1919. className: "b nobr",
  1920. });
  1921. })();
  1922.  
  1923. return [content, filterMode, reason];
  1924. }
  1925.  
  1926. /**
  1927. * 初始化组件
  1928. */
  1929. initComponents() {
  1930. super.initComponents();
  1931.  
  1932. const { tabs, content } = this.ui.views;
  1933.  
  1934. const table = this.ui.createTable(this.columns());
  1935.  
  1936. const tab = this.ui.createTab(
  1937. tabs,
  1938. this.constructor.label,
  1939. this.constructor.order,
  1940. {
  1941. onclick: () => {
  1942. this.render(content);
  1943. },
  1944. }
  1945. );
  1946.  
  1947. Object.assign(this.views, {
  1948. tab,
  1949. table,
  1950. });
  1951.  
  1952. this.views.container.appendChild(table);
  1953. }
  1954.  
  1955. /**
  1956. * 渲染
  1957. * @param {HTMLElement} container 容器
  1958. */
  1959. render(container) {
  1960. super.render(container);
  1961.  
  1962. const { table } = this.views;
  1963.  
  1964. if (table) {
  1965. const { add, clear } = table;
  1966.  
  1967. clear();
  1968.  
  1969. const list = this.data.filter((item) => {
  1970. return (item.filterMode || "显示") !== "显示";
  1971. });
  1972.  
  1973. Object.values(list).forEach((item) => {
  1974. const column = this.column(item);
  1975.  
  1976. add(...column);
  1977. });
  1978. }
  1979. }
  1980.  
  1981. /**
  1982. * 通知
  1983. * @param {*} item 绑定的 nFilter
  1984. */
  1985. async notify() {
  1986. // 获取过滤后的数量
  1987. const count = this.data.filter((item) => {
  1988. return (item.filterMode || "显示") !== "显示";
  1989. }).length;
  1990.  
  1991. // 更新菜单文字
  1992. const { ui } = this;
  1993. const { menu } = ui;
  1994.  
  1995. if (menu === null) {
  1996. return;
  1997. }
  1998.  
  1999. if (count) {
  2000. menu.innerHTML = `${ui.constructor.label} <span class="small_colored_text_btn stxt block_txt_c0 vertmod">${count}</span>`;
  2001. } else {
  2002. menu.innerHTML = `${ui.constructor.label}`;
  2003. }
  2004.  
  2005. // 重新渲染
  2006. // TODO 应该给 table 增加一个判重的逻辑,这样只需要更新过滤后的内容即可
  2007. const { tab } = this.views;
  2008.  
  2009. if (tab.querySelector("A").className === "nobr") {
  2010. this.render(ui.views.content);
  2011. }
  2012. }
  2013. }
  2014.  
  2015. /**
  2016. * 用户模块
  2017. */
  2018. class UserModule extends Module {
  2019. /**
  2020. * 模块名称
  2021. */
  2022. static name = "user";
  2023.  
  2024. /**
  2025. * 模块标签
  2026. */
  2027. static label = "用户";
  2028.  
  2029. /**
  2030. * 顺序
  2031. */
  2032. static order = 20;
  2033.  
  2034. /**
  2035. * 获取列表
  2036. */
  2037. get list() {
  2038. return this.settings.users;
  2039. }
  2040.  
  2041. /**
  2042. * 获取用户
  2043. * @param {Number} uid 用户 ID
  2044. */
  2045. get(uid) {
  2046. // 获取列表
  2047. const list = this.list;
  2048.  
  2049. // 如果存在,则返回信息
  2050. if (list[uid]) {
  2051. return list[uid];
  2052. }
  2053.  
  2054. return null;
  2055. }
  2056.  
  2057. /**
  2058. * 添加用户
  2059. * @param {Number} uid 用户 ID
  2060. */
  2061. add(uid, values) {
  2062. // 获取列表
  2063. const list = this.list;
  2064.  
  2065. // 如果已存在,则返回信息
  2066. if (list[uid]) {
  2067. return list[uid];
  2068. }
  2069.  
  2070. // 写入用户信息
  2071. list[uid] = values;
  2072.  
  2073. // 保存数据
  2074. this.settings.users = list;
  2075.  
  2076. // 重新过滤
  2077. this.reFilter(uid);
  2078.  
  2079. // 返回添加的用户
  2080. return values;
  2081. }
  2082.  
  2083. /**
  2084. * 编辑用户
  2085. * @param {Number} uid 用户 ID
  2086. * @param {*} values 用户信息
  2087. */
  2088. update(uid, values) {
  2089. // 获取列表
  2090. const list = this.list;
  2091.  
  2092. // 如果不存在则跳过
  2093. if (Object.hasOwn(list, uid) === false) {
  2094. return null;
  2095. }
  2096.  
  2097. // 获取用户
  2098. const entity = list[uid];
  2099.  
  2100. // 更新用户
  2101. Object.assign(entity, values);
  2102.  
  2103. // 保存数据
  2104. this.settings.users = list;
  2105.  
  2106. // 重新过滤
  2107. this.reFilter(uid);
  2108.  
  2109. // 返回编辑的用户
  2110. return entity;
  2111. }
  2112.  
  2113. /**
  2114. * 删除用户
  2115. * @param {Number} uid 用户 ID
  2116. * @returns {Object | null} 删除的用户
  2117. */
  2118. remove(uid) {
  2119. // 获取列表
  2120. const list = this.list;
  2121.  
  2122. // 如果不存在则跳过
  2123. if (Object.hasOwn(list, uid) === false) {
  2124. return null;
  2125. }
  2126.  
  2127. // 获取用户
  2128. const entity = list[uid];
  2129.  
  2130. // 删除用户
  2131. delete list[uid];
  2132.  
  2133. // 保存数据
  2134. this.settings.users = list;
  2135.  
  2136. // 重新过滤
  2137. this.reFilter(uid);
  2138.  
  2139. // 返回删除的用户
  2140. return entity;
  2141. }
  2142.  
  2143. /**
  2144. * 格式化
  2145. * @param {Number} uid 用户 ID
  2146. * @param {String | undefined} name 用户名称
  2147. */
  2148. format(uid, name) {
  2149. if (uid <= 0) {
  2150. return null;
  2151. }
  2152.  
  2153. const { ui } = this;
  2154.  
  2155. const user = this.get(uid);
  2156.  
  2157. if (user) {
  2158. name = user.name;
  2159. }
  2160.  
  2161. const username = name ? "@" + name : "#" + uid;
  2162.  
  2163. return ui.createElement("A", `[${username}]`, {
  2164. className: "b nobr",
  2165. href: `/nuke.php?func=ucp&uid=${uid}`,
  2166. });
  2167. }
  2168.  
  2169. /**
  2170. * 表格列
  2171. * @returns {Array} 表格列集合
  2172. */
  2173. columns() {
  2174. return [
  2175. { label: "昵称" },
  2176. { label: "过滤模式", center: true, width: 1 },
  2177. { label: "操作", width: 1 },
  2178. ];
  2179. }
  2180.  
  2181. /**
  2182. * 表格项
  2183. * @param {*} item 用户信息
  2184. * @returns {Array} 表格项集合
  2185. */
  2186. column(item) {
  2187. const { ui } = this;
  2188. const { table } = this.views;
  2189. const { id, name, filterMode } = item;
  2190.  
  2191. // 昵称
  2192. const user = this.format(id, name);
  2193.  
  2194. // 切换过滤模式
  2195. const switchMode = ui.createButton(
  2196. filterMode || this.settings.filterModes[0],
  2197. () => {
  2198. const newMode = this.settings.switchModeByName(switchMode.innerText);
  2199.  
  2200. this.update(id, {
  2201. filterMode: newMode,
  2202. });
  2203.  
  2204. switchMode.innerText = newMode;
  2205. }
  2206. );
  2207.  
  2208. // 操作
  2209. const buttons = (() => {
  2210. const remove = ui.createButton("删除", (e) => {
  2211. ui.confirm().then(() => {
  2212. this.remove(id);
  2213.  
  2214. table.remove(e);
  2215. });
  2216. });
  2217.  
  2218. return ui.createButtonGroup(remove);
  2219. })();
  2220.  
  2221. return [user, switchMode, buttons];
  2222. }
  2223.  
  2224. /**
  2225. * 初始化组件
  2226. */
  2227. initComponents() {
  2228. super.initComponents();
  2229.  
  2230. const { ui } = this;
  2231. const { tabs, content, settings } = ui.views;
  2232. const { add } = settings;
  2233.  
  2234. const table = ui.createTable(this.columns());
  2235.  
  2236. const tab = ui.createTab(
  2237. tabs,
  2238. this.constructor.label,
  2239. this.constructor.order,
  2240. {
  2241. onclick: () => {
  2242. this.render(content);
  2243. },
  2244. }
  2245. );
  2246.  
  2247. const keywordFilter = (() => {
  2248. const input = ui.createElement("INPUT", [], {
  2249. style: "flex: 1;",
  2250. placeholder: "输入昵称关键字进行筛选",
  2251. });
  2252.  
  2253. const button = ui.createButton("筛选", () => {
  2254. this.render(content, input.value);
  2255. });
  2256.  
  2257. const wrapper = ui.createElement("DIV", [input, button], {
  2258. style: "display: flex; margin-top: 10px;",
  2259. });
  2260.  
  2261. return wrapper;
  2262. })();
  2263.  
  2264. Object.assign(this.views, {
  2265. tab,
  2266. table,
  2267. keywordFilter,
  2268. });
  2269.  
  2270. this.views.container.appendChild(table);
  2271. this.views.container.appendChild(keywordFilter);
  2272.  
  2273. // 删除非激活中的用户
  2274. {
  2275. const list = ui.createElement("DIV", [], {
  2276. style: "white-space: normal;",
  2277. });
  2278.  
  2279. const button = ui.createButton("删除非激活中的用户", () => {
  2280. ui.confirm().then(() => {
  2281. list.innerHTML = "";
  2282.  
  2283. const users = Object.values(this.list);
  2284.  
  2285. const waitingQueue = users.map(
  2286. ({ id }) =>
  2287. () =>
  2288. this.api.getUserInfo(id).then(({ bit }) => {
  2289. const activeInfo = commonui.activeInfo(0, 0, bit);
  2290. const activeType = activeInfo[1];
  2291.  
  2292. if (["ACTIVED", "LINKED"].includes(activeType)) {
  2293. return;
  2294. }
  2295.  
  2296. list.append(this.format(id));
  2297.  
  2298. this.remove(id);
  2299. })
  2300. );
  2301.  
  2302. const queueLength = waitingQueue.length;
  2303.  
  2304. const execute = () => {
  2305. if (waitingQueue.length) {
  2306. const next = waitingQueue.shift();
  2307.  
  2308. button.disabled = true;
  2309. button.innerHTML = `删除非激活中的用户 (${
  2310. queueLength - waitingQueue.length
  2311. }/${queueLength})`;
  2312.  
  2313. next().finally(execute);
  2314. return;
  2315. }
  2316.  
  2317. button.disabled = false;
  2318. };
  2319.  
  2320. execute();
  2321. });
  2322. });
  2323.  
  2324. const element = ui.createElement("DIV", [button, list]);
  2325.  
  2326. add(this.constructor.order + 0, element);
  2327. }
  2328. }
  2329.  
  2330. /**
  2331. * 渲染
  2332. * @param {HTMLElement} container 容器
  2333. * @param {String} keyword 关键字
  2334. */
  2335. render(container, keyword = "") {
  2336. super.render(container);
  2337.  
  2338. const { table } = this.views;
  2339.  
  2340. if (table) {
  2341. const { add, clear } = table;
  2342.  
  2343. clear();
  2344.  
  2345. const list = keyword
  2346. ? Object.values(this.list).filter((item) =>
  2347. item.name.includes(keyword)
  2348. )
  2349. : Object.values(this.list);
  2350.  
  2351. list.forEach((item) => {
  2352. const column = this.column(item);
  2353.  
  2354. add(...column);
  2355. });
  2356. }
  2357. }
  2358.  
  2359. /**
  2360. * 渲染详情
  2361. * @param {Number} uid 用户 ID
  2362. * @param {String | undefined} name 用户名称
  2363. * @param {Function} callback 回调函数
  2364. */
  2365. renderDetails(uid, name, callback = () => {}) {
  2366. const { ui, settings } = this;
  2367.  
  2368. // 只允许同时存在一个详情页
  2369. if (this.views.details) {
  2370. if (this.views.details.parentNode) {
  2371. this.views.details.parentNode.removeChild(this.views.details);
  2372. }
  2373. }
  2374.  
  2375. // 获取用户信息
  2376. const user = this.get(uid);
  2377.  
  2378. if (user) {
  2379. name = user.name;
  2380. }
  2381.  
  2382. const title =
  2383. (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;
  2384.  
  2385. const filterMode = user ? user.filterMode : settings.filterModes[0];
  2386.  
  2387. const switchMode = ui.createButton(filterMode, () => {
  2388. const newMode = settings.switchModeByName(switchMode.innerText);
  2389.  
  2390. switchMode.innerText = newMode;
  2391. });
  2392.  
  2393. const buttons = ui.createElement(
  2394. "DIV",
  2395. (() => {
  2396. const remove = user
  2397. ? ui.createButton("删除", () => {
  2398. ui.confirm().then(() => {
  2399. this.remove(uid);
  2400.  
  2401. this.views.details._.hide();
  2402.  
  2403. callback("REMOVE");
  2404. });
  2405. })
  2406. : null;
  2407.  
  2408. const save = ui.createButton("保存", () => {
  2409. if (user === null) {
  2410. const entity = this.add(uid, {
  2411. id: uid,
  2412. name,
  2413. tags: [],
  2414. filterMode: switchMode.innerText,
  2415. });
  2416.  
  2417. this.views.details._.hide();
  2418.  
  2419. callback("ADD", entity);
  2420. } else {
  2421. const entity = this.update(uid, {
  2422. name,
  2423. filterMode: switchMode.innerText,
  2424. });
  2425.  
  2426. this.views.details._.hide();
  2427.  
  2428. callback("UPDATE", entity);
  2429. }
  2430. });
  2431.  
  2432. return ui.createButtonGroup(remove, save);
  2433. })(),
  2434. {
  2435. className: "right_",
  2436. }
  2437. );
  2438.  
  2439. const actions = ui.createElement(
  2440. "DIV",
  2441. [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
  2442. {
  2443. style: "margin-top: 10px;",
  2444. }
  2445. );
  2446.  
  2447. const tips = ui.createElement("DIV", TIPS.filterMode, {
  2448. className: "silver",
  2449. style: "margin-top: 10px;",
  2450. });
  2451.  
  2452. const content = ui.createElement("DIV", [actions, tips], {
  2453. style: "width: 80vw",
  2454. });
  2455.  
  2456. // 创建弹出框
  2457. this.views.details = ui.createDialog(null, title, content);
  2458. }
  2459.  
  2460. /**
  2461. * 过滤
  2462. * @param {*} item 绑定的 nFilter
  2463. * @param {*} result 过滤结果
  2464. */
  2465. async filter(item, result) {
  2466. // 获取用户信息
  2467. const user = this.get(item.uid);
  2468.  
  2469. // 没有则跳过
  2470. if (user === null) {
  2471. return;
  2472. }
  2473.  
  2474. // 获取用户过滤模式
  2475. const mode = this.settings.getModeByName(user.filterMode);
  2476.  
  2477. // 不高于当前过滤模式则跳过
  2478. if (mode <= result.mode) {
  2479. return;
  2480. }
  2481.  
  2482. // 更新过滤模式和原因
  2483. result.mode = mode;
  2484. result.reason = `用户模式: ${user.filterMode}`;
  2485. }
  2486.  
  2487. /**
  2488. * 通知
  2489. * @param {*} item 绑定的 nFilter
  2490. */
  2491. async notify(item) {
  2492. const { uid, username, action } = item;
  2493.  
  2494. // 如果没有 action 组件则跳过
  2495. if (action === null) {
  2496. return;
  2497. }
  2498.  
  2499. // 如果是匿名,隐藏组件
  2500. if (uid <= 0) {
  2501. action.style.display = "none";
  2502. return;
  2503. }
  2504.  
  2505. // 获取当前用户
  2506. const user = this.get(uid);
  2507.  
  2508. // 修改操作按钮文字
  2509. if (action.tagName === "A") {
  2510. action.innerText = "屏蔽";
  2511. } else {
  2512. action.title = "屏蔽";
  2513. }
  2514.  
  2515. // 修改操作按钮颜色
  2516. if (user) {
  2517. action.style.background = "#CB4042";
  2518. } else {
  2519. action.style.background = "#AAA";
  2520. }
  2521.  
  2522. // 绑定事件
  2523. action.onclick = () => {
  2524. this.renderDetails(uid, username);
  2525. };
  2526. }
  2527.  
  2528. /**
  2529. * 重新过滤
  2530. * @param {Number} uid 用户 ID
  2531. */
  2532. reFilter(uid) {
  2533. this.data.forEach((item) => {
  2534. // 如果用户 ID 一致,则重新过滤
  2535. if (item.uid === uid) {
  2536. item.execute();
  2537. return;
  2538. }
  2539.  
  2540. // 如果有引用,也重新过滤
  2541. if (Object.hasOwn(item.quotes || {}, uid)) {
  2542. item.execute();
  2543. return;
  2544. }
  2545. });
  2546. }
  2547. }
  2548.  
  2549. /**
  2550. * 标记模块
  2551. */
  2552. class TagModule extends Module {
  2553. /**
  2554. * 模块名称
  2555. */
  2556. static name = "tag";
  2557.  
  2558. /**
  2559. * 模块标签
  2560. */
  2561. static label = "标记";
  2562.  
  2563. /**
  2564. * 顺序
  2565. */
  2566. static order = 30;
  2567.  
  2568. /**
  2569. * 依赖模块
  2570. */
  2571. static depends = [UserModule];
  2572.  
  2573. /**
  2574. * 依赖的用户模块
  2575. * @returns {UserModule} 用户模块
  2576. */
  2577. get userModule() {
  2578. return this.depends[UserModule.name];
  2579. }
  2580.  
  2581. /**
  2582. * 获取列表
  2583. */
  2584. get list() {
  2585. return this.settings.tags;
  2586. }
  2587.  
  2588. /**
  2589. * 获取标记
  2590. * @param {Number} id 标记 ID
  2591. * @param {String} name 标记名称
  2592. */
  2593. get({ id, name }) {
  2594. // 获取列表
  2595. const list = this.list;
  2596.  
  2597. // 通过 ID 获取标记
  2598. if (list[id]) {
  2599. return list[id];
  2600. }
  2601.  
  2602. // 通过名称获取标记
  2603. if (name) {
  2604. const tag = Object.values(list).find((item) => item.name === name);
  2605.  
  2606. if (tag) {
  2607. return tag;
  2608. }
  2609. }
  2610.  
  2611. return null;
  2612. }
  2613.  
  2614. /**
  2615. * 添加标记
  2616. * @param {String} name 标记名称
  2617. */
  2618. add(name) {
  2619. // 获取对应的标记
  2620. const tag = this.get({ name });
  2621.  
  2622. // 如果标记已存在,则返回标记信息,否则增加标记
  2623. if (tag) {
  2624. return tag;
  2625. }
  2626.  
  2627. // 获取列表
  2628. const list = this.list;
  2629.  
  2630. // ID 为最大值 + 1
  2631. const id = Math.max(...Object.keys(list), 0) + 1;
  2632.  
  2633. // 标记的颜色
  2634. const color = Tools.generateColor(name);
  2635.  
  2636. // 写入标记信息
  2637. list[id] = {
  2638. id,
  2639. name,
  2640. color,
  2641. filterMode: this.settings.filterModes[0],
  2642. };
  2643.  
  2644. // 保存数据
  2645. this.settings.tags = list;
  2646.  
  2647. // 返回添加的标记
  2648. return list[id];
  2649. }
  2650.  
  2651. /**
  2652. * 编辑标记
  2653. * @param {Number} id 标记 ID
  2654. * @param {*} values 标记信息
  2655. */
  2656. update(id, values) {
  2657. // 获取列表
  2658. const list = this.list;
  2659.  
  2660. // 如果不存在则跳过
  2661. if (Object.hasOwn(list, id) === false) {
  2662. return null;
  2663. }
  2664.  
  2665. // 获取标记
  2666. const entity = list[id];
  2667.  
  2668. // 获取相关的用户
  2669. const users = Object.values(this.userModule.list).filter((user) =>
  2670. user.tags.includes(id)
  2671. );
  2672.  
  2673. // 更新标记
  2674. Object.assign(entity, values);
  2675.  
  2676. // 保存数据
  2677. this.settings.tags = list;
  2678.  
  2679. // 重新过滤
  2680. this.reFilter(users);
  2681. }
  2682.  
  2683. /**
  2684. * 删除标记
  2685. * @param {Number} id 标记 ID
  2686. */
  2687. remove(id) {
  2688. // 获取列表
  2689. const list = this.list;
  2690.  
  2691. // 如果不存在则跳过
  2692. if (Object.hasOwn(list, id) === false) {
  2693. return null;
  2694. }
  2695.  
  2696. // 获取标记
  2697. const entity = list[id];
  2698.  
  2699. // 获取相关的用户
  2700. const users = Object.values(this.userModule.list).filter((user) =>
  2701. user.tags.includes(id)
  2702. );
  2703.  
  2704. // 删除标记
  2705. delete list[id];
  2706.  
  2707. // 删除相关的用户标记
  2708. users.forEach((user) => {
  2709. const index = user.tags.findIndex((item) => item === id);
  2710.  
  2711. if (index >= 0) {
  2712. user.tags.splice(index, 1);
  2713. }
  2714. });
  2715.  
  2716. // 保存数据
  2717. this.settings.tags = list;
  2718.  
  2719. // 重新过滤
  2720. this.reFilter(users);
  2721.  
  2722. // 返回删除的标记
  2723. return entity;
  2724. }
  2725.  
  2726. /**
  2727. * 格式化
  2728. * @param {Number} id 标记 ID
  2729. * @param {String | undefined} name 标记名称
  2730. * @param {String | undefined} name 标记颜色
  2731. */
  2732. format(id, name, color) {
  2733. const { ui } = this;
  2734.  
  2735. if (id >= 0) {
  2736. const tag = this.get({ id });
  2737.  
  2738. if (tag) {
  2739. name = tag.name;
  2740. color = tag.color;
  2741. }
  2742. }
  2743.  
  2744. if (name && color) {
  2745. return ui.createElement("B", name, {
  2746. className: "block_txt nobr",
  2747. style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
  2748. });
  2749. }
  2750.  
  2751. return "";
  2752. }
  2753.  
  2754. /**
  2755. * 表格列
  2756. * @returns {Array} 表格列集合
  2757. */
  2758. columns() {
  2759. return [
  2760. { label: "标记", width: 1 },
  2761. { label: "列表" },
  2762. { label: "过滤模式", width: 1 },
  2763. { label: "操作", width: 1 },
  2764. ];
  2765. }
  2766.  
  2767. /**
  2768. * 表格项
  2769. * @param {*} item 标记信息
  2770. * @returns {Array} 表格项集合
  2771. */
  2772. column(item) {
  2773. const { ui } = this;
  2774. const { table } = this.views;
  2775. const { id, filterMode } = item;
  2776.  
  2777. // 标记
  2778. const tag = this.format(id);
  2779.  
  2780. // 用户列表
  2781. const list = Object.values(this.userModule.list)
  2782. .filter(({ tags }) => tags.includes(id))
  2783. .map(({ id }) => this.userModule.format(id));
  2784.  
  2785. const group = ui.createElement("DIV", list, {
  2786. style: "white-space: normal; display: none;",
  2787. });
  2788.  
  2789. const switchButton = ui.createButton(list.length.toString(), () => {
  2790. if (group.style.display === "none") {
  2791. group.style.removeProperty("display");
  2792. } else {
  2793. group.style.display = "none";
  2794. }
  2795. });
  2796.  
  2797. // 切换过滤模式
  2798. const switchMode = ui.createButton(
  2799. filterMode || this.settings.filterModes[0],
  2800. () => {
  2801. const newMode = this.settings.switchModeByName(switchMode.innerText);
  2802.  
  2803. this.update(id, {
  2804. filterMode: newMode,
  2805. });
  2806.  
  2807. switchMode.innerText = newMode;
  2808. }
  2809. );
  2810.  
  2811. // 操作
  2812. const buttons = (() => {
  2813. const remove = ui.createButton("删除", (e) => {
  2814. ui.confirm().then(() => {
  2815. this.remove(id);
  2816.  
  2817. table.remove(e);
  2818. });
  2819. });
  2820.  
  2821. return ui.createButtonGroup(remove);
  2822. })();
  2823.  
  2824. return [tag, [switchButton, group], switchMode, buttons];
  2825. }
  2826.  
  2827. /**
  2828. * 初始化组件
  2829. */
  2830. initComponents() {
  2831. super.initComponents();
  2832.  
  2833. const { ui } = this;
  2834. const { tabs, content, settings } = ui.views;
  2835. const { add } = settings;
  2836.  
  2837. const table = ui.createTable(this.columns());
  2838.  
  2839. const tab = ui.createTab(
  2840. tabs,
  2841. this.constructor.label,
  2842. this.constructor.order,
  2843. {
  2844. onclick: () => {
  2845. this.render(content);
  2846. },
  2847. }
  2848. );
  2849.  
  2850. Object.assign(this.views, {
  2851. tab,
  2852. table,
  2853. });
  2854.  
  2855. this.views.container.appendChild(table);
  2856.  
  2857. // 删除没有标记的用户
  2858. {
  2859. const button = ui.createButton("删除没有标记的用户", () => {
  2860. ui.confirm().then(() => {
  2861. const users = Object.values(this.userModule.list);
  2862.  
  2863. users.forEach(({ id, tags }) => {
  2864. if (tags.length > 0) {
  2865. return;
  2866. }
  2867.  
  2868. this.userModule.remove(id);
  2869. });
  2870. });
  2871. });
  2872.  
  2873. const element = ui.createElement("DIV", button);
  2874.  
  2875. add(this.constructor.order + 0, element);
  2876. }
  2877.  
  2878. // 删除没有用户的标记
  2879. {
  2880. const button = ui.createButton("删除没有用户的标记", () => {
  2881. ui.confirm().then(() => {
  2882. const items = Object.values(this.list);
  2883. const users = Object.values(this.userModule.list);
  2884.  
  2885. items.forEach(({ id }) => {
  2886. if (users.find(({ tags }) => tags.includes(id))) {
  2887. return;
  2888. }
  2889.  
  2890. this.remove(id);
  2891. });
  2892. });
  2893. });
  2894.  
  2895. const element = ui.createElement("DIV", button);
  2896.  
  2897. add(this.constructor.order + 1, element);
  2898. }
  2899. }
  2900.  
  2901. /**
  2902. * 渲染
  2903. * @param {HTMLElement} container 容器
  2904. */
  2905. render(container) {
  2906. super.render(container);
  2907.  
  2908. const { table } = this.views;
  2909.  
  2910. if (table) {
  2911. const { add, clear } = table;
  2912.  
  2913. clear();
  2914.  
  2915. Object.values(this.list).forEach((item) => {
  2916. const column = this.column(item);
  2917.  
  2918. add(...column);
  2919. });
  2920. }
  2921. }
  2922.  
  2923. /**
  2924. * 过滤
  2925. * @param {*} item 绑定的 nFilter
  2926. * @param {*} result 过滤结果
  2927. */
  2928. async filter(item, result) {
  2929. // 获取用户信息
  2930. const user = this.userModule.get(item.uid);
  2931.  
  2932. // 没有则跳过
  2933. if (user === null) {
  2934. return;
  2935. }
  2936.  
  2937. // 获取用户标记
  2938. const tags = user.tags;
  2939.  
  2940. // 取最高的过滤模式
  2941. // 低于当前的过滤模式则跳过
  2942. let max = result.mode;
  2943. let tag = null;
  2944.  
  2945. for (const id of tags) {
  2946. const entity = this.get({ id });
  2947.  
  2948. if (entity === null) {
  2949. continue;
  2950. }
  2951.  
  2952. // 获取过滤模式
  2953. const mode = this.settings.getModeByName(entity.filterMode);
  2954.  
  2955. if (mode < max) {
  2956. continue;
  2957. }
  2958.  
  2959. if (mode === max && result.reason.includes("用户模式") === false) {
  2960. continue;
  2961. }
  2962.  
  2963. max = mode;
  2964. tag = entity;
  2965. }
  2966.  
  2967. // 没有匹配的则跳过
  2968. if (tag === null) {
  2969. return;
  2970. }
  2971.  
  2972. // 更新过滤模式和原因
  2973. result.mode = max;
  2974. result.reason = `标记: ${tag.name}`;
  2975. }
  2976.  
  2977. /**
  2978. * 通知
  2979. * @param {*} item 绑定的 nFilter
  2980. */
  2981. async notify(item) {
  2982. const { uid, tags } = item;
  2983.  
  2984. // 如果没有 tags 组件则跳过
  2985. if (tags === null) {
  2986. return;
  2987. }
  2988.  
  2989. // 如果是匿名,隐藏组件
  2990. if (uid <= 0) {
  2991. tags.style.display = "none";
  2992. return;
  2993. }
  2994.  
  2995. // 删除旧标记
  2996. [...tags.querySelectorAll("[tid]")].forEach((item) => {
  2997. tags.removeChild(item);
  2998. });
  2999.  
  3000. // 获取当前用户
  3001. const user = this.userModule.get(uid);
  3002.  
  3003. // 如果没有用户,则跳过
  3004. if (user === null) {
  3005. return;
  3006. }
  3007.  
  3008. // 格式化标记
  3009. const items = user.tags.map((id) => {
  3010. const item = this.format(id);
  3011.  
  3012. if (item) {
  3013. item.setAttribute("tid", id);
  3014. }
  3015.  
  3016. return item;
  3017. });
  3018.  
  3019. // 加入组件
  3020. items.forEach((item) => {
  3021. if (item) {
  3022. tags.appendChild(item);
  3023. }
  3024. });
  3025. }
  3026.  
  3027. /**
  3028. * 重新过滤
  3029. * @param {Array} users 用户集合
  3030. */
  3031. reFilter(users) {
  3032. users.forEach((user) => {
  3033. this.userModule.reFilter(user.id);
  3034. });
  3035. }
  3036. }
  3037.  
  3038. /**
  3039. * 关键字模块
  3040. */
  3041. class KeywordModule extends Module {
  3042. /**
  3043. * 模块名称
  3044. */
  3045. static name = "keyword";
  3046.  
  3047. /**
  3048. * 模块标签
  3049. */
  3050. static label = "关键字";
  3051.  
  3052. /**
  3053. * 顺序
  3054. */
  3055. static order = 40;
  3056.  
  3057. /**
  3058. * 获取列表
  3059. */
  3060. get list() {
  3061. return this.settings.keywords;
  3062. }
  3063.  
  3064. /**
  3065. * 将多个布尔值转换为二进制
  3066. */
  3067. boolsToBinary(...args) {
  3068. let res = 0;
  3069.  
  3070. for (let i = 0; i < args.length; i += 1) {
  3071. if (args[i]) {
  3072. res |= 1 << i;
  3073. }
  3074. }
  3075.  
  3076. return res;
  3077. }
  3078.  
  3079. /**
  3080. * 获取关键字
  3081. * @param {Number} id 关键字 ID
  3082. */
  3083. get(id) {
  3084. // 获取列表
  3085. const list = this.list;
  3086.  
  3087. // 如果存在,则返回信息
  3088. if (list[id]) {
  3089. return list[id];
  3090. }
  3091.  
  3092. return null;
  3093. }
  3094.  
  3095. /**
  3096. * 添加关键字
  3097. * @param {String} keyword 关键字
  3098. * @param {String} filterMode 过滤模式
  3099. * @param {Number} filterType 过滤类型,为一个二进制数,0b1 - 过滤标题,0b10 - 过滤内容,0b100 - 过滤昵称
  3100. */
  3101. add(keyword, filterMode, filterType) {
  3102. // 获取列表
  3103. const list = this.list;
  3104.  
  3105. // ID 为最大值 + 1
  3106. const id = Math.max(...Object.keys(list), 0) + 1;
  3107.  
  3108. // 写入关键字信息
  3109. list[id] = {
  3110. id,
  3111. keyword,
  3112. filterMode,
  3113. filterType,
  3114. };
  3115.  
  3116. // 保存数据
  3117. this.settings.keywords = list;
  3118.  
  3119. // 重新过滤
  3120. this.reFilter();
  3121.  
  3122. // 返回添加的关键字
  3123. return list[id];
  3124. }
  3125.  
  3126. /**
  3127. * 编辑关键字
  3128. * @param {Number} id 关键字 ID
  3129. * @param {*} values 关键字信息
  3130. */
  3131. update(id, values) {
  3132. // 获取列表
  3133. const list = this.list;
  3134.  
  3135. // 如果不存在则跳过
  3136. if (Object.hasOwn(list, id) === false) {
  3137. return null;
  3138. }
  3139.  
  3140. // 获取关键字
  3141. const entity = list[id];
  3142.  
  3143. // 更新关键字
  3144. Object.assign(entity, values);
  3145.  
  3146. // 保存数据
  3147. this.settings.keywords = list;
  3148.  
  3149. // 重新过滤
  3150. this.reFilter();
  3151. }
  3152.  
  3153. /**
  3154. * 删除关键字
  3155. * @param {Number} id 关键字 ID
  3156. */
  3157. remove(id) {
  3158. // 获取列表
  3159. const list = this.list;
  3160.  
  3161. // 如果不存在则跳过
  3162. if (Object.hasOwn(list, id) === false) {
  3163. return null;
  3164. }
  3165.  
  3166. // 获取关键字
  3167. const entity = list[id];
  3168.  
  3169. // 删除关键字
  3170. delete list[id];
  3171.  
  3172. // 保存数据
  3173. this.settings.keywords = list;
  3174.  
  3175. // 重新过滤
  3176. this.reFilter();
  3177.  
  3178. // 返回删除的关键字
  3179. return entity;
  3180. }
  3181.  
  3182. /**
  3183. * 获取帖子数据
  3184. * @param {*} item 绑定的 nFilter
  3185. */
  3186. async getPostInfo(item) {
  3187. const { tid, pid } = item;
  3188.  
  3189. // 请求帖子数据
  3190. const { subject, content, userInfo, reputation } =
  3191. await this.api.getPostInfo(tid, pid);
  3192.  
  3193. // 绑定用户信息和声望
  3194. if (userInfo) {
  3195. item.userInfo = userInfo;
  3196. item.username = userInfo.username;
  3197. item.reputation = reputation;
  3198. }
  3199.  
  3200. // 绑定标题和内容
  3201. item.subject = subject;
  3202. item.content = content;
  3203. }
  3204.  
  3205. /**
  3206. * 表格列
  3207. * @returns {Array} 表格列集合
  3208. */
  3209. columns() {
  3210. return [
  3211. { label: "关键字" },
  3212. { label: "过滤模式", center: true, width: 1 },
  3213. { label: "过滤标题", center: true, width: 1 },
  3214. { label: "过滤内容", center: true, width: 1 },
  3215. { label: "过滤昵称", center: true, width: 1 },
  3216. { label: "操作", width: 1 },
  3217. ];
  3218. }
  3219.  
  3220. /**
  3221. * 表格项
  3222. * @param {*} item 标记信息
  3223. * @returns {Array} 表格项集合
  3224. */
  3225. column(item) {
  3226. const { ui } = this;
  3227. const { table } = this.views;
  3228. const { id, keyword, filterMode } = item;
  3229.  
  3230. // 兼容旧版本数据
  3231. const filterType =
  3232. item.filterType !== undefined
  3233. ? item.filterType
  3234. : item.filterLevel > 0
  3235. ? 0b11
  3236. : 0b01;
  3237.  
  3238. // 关键字
  3239. const input = ui.createElement("INPUT", [], {
  3240. type: "text",
  3241. value: keyword,
  3242. });
  3243.  
  3244. const inputWrapper = ui.createElement("DIV", input, {
  3245. className: "filter-input-wrapper",
  3246. });
  3247.  
  3248. // 切换过滤模式
  3249. const switchMode = ui.createButton(
  3250. filterMode || this.settings.filterModes[0],
  3251. () => {
  3252. const newMode = this.settings.switchModeByName(switchMode.innerText);
  3253.  
  3254. switchMode.innerText = newMode;
  3255. }
  3256. );
  3257.  
  3258. // 过滤标题
  3259. const switchTitle = ui.createElement("INPUT", [], {
  3260. type: "checkbox",
  3261. checked: filterType & 0b1,
  3262. });
  3263.  
  3264. // 过滤内容
  3265. const switchContent = ui.createElement("INPUT", [], {
  3266. type: "checkbox",
  3267. checked: filterType & 0b10,
  3268. });
  3269.  
  3270. // 过滤昵称
  3271. const switchUsername = ui.createElement("INPUT", [], {
  3272. type: "checkbox",
  3273. checked: filterType & 0b100,
  3274. });
  3275.  
  3276. // 操作
  3277. const buttons = (() => {
  3278. const save = ui.createButton("保存", () => {
  3279. this.update(id, {
  3280. keyword: input.value,
  3281. filterMode: switchMode.innerText,
  3282. filterType: this.boolsToBinary(
  3283. switchTitle.checked,
  3284. switchContent.checked,
  3285. switchUsername.checked
  3286. ),
  3287. });
  3288. });
  3289.  
  3290. const remove = ui.createButton("删除", (e) => {
  3291. ui.confirm().then(() => {
  3292. this.remove(id);
  3293.  
  3294. table.remove(e);
  3295. });
  3296. });
  3297.  
  3298. return ui.createButtonGroup(save, remove);
  3299. })();
  3300.  
  3301. return [
  3302. inputWrapper,
  3303. switchMode,
  3304. switchTitle,
  3305. switchContent,
  3306. switchUsername,
  3307. buttons,
  3308. ];
  3309. }
  3310.  
  3311. /**
  3312. * 初始化组件
  3313. */
  3314. initComponents() {
  3315. super.initComponents();
  3316.  
  3317. const { ui } = this;
  3318. const { tabs, content } = ui.views;
  3319.  
  3320. const table = ui.createTable(this.columns());
  3321.  
  3322. const tips = ui.createElement("DIV", TIPS.keyword, {
  3323. className: "silver",
  3324. });
  3325.  
  3326. const tab = ui.createTab(
  3327. tabs,
  3328. this.constructor.label,
  3329. this.constructor.order,
  3330. {
  3331. onclick: () => {
  3332. this.render(content);
  3333. },
  3334. }
  3335. );
  3336.  
  3337. Object.assign(this.views, {
  3338. tab,
  3339. table,
  3340. });
  3341.  
  3342. this.views.container.appendChild(table);
  3343. this.views.container.appendChild(tips);
  3344. }
  3345.  
  3346. /**
  3347. * 渲染
  3348. * @param {HTMLElement} container 容器
  3349. */
  3350. render(container) {
  3351. super.render(container);
  3352.  
  3353. const { table } = this.views;
  3354.  
  3355. if (table) {
  3356. const { add, clear } = table;
  3357.  
  3358. clear();
  3359.  
  3360. Object.values(this.list).forEach((item) => {
  3361. const column = this.column(item);
  3362.  
  3363. add(...column);
  3364. });
  3365.  
  3366. this.renderNewLine();
  3367. }
  3368. }
  3369.  
  3370. /**
  3371. * 渲染新行
  3372. */
  3373. renderNewLine() {
  3374. const { ui } = this;
  3375. const { table } = this.views;
  3376.  
  3377. // 关键字
  3378. const input = ui.createElement("INPUT", [], {
  3379. type: "text",
  3380. });
  3381.  
  3382. const inputWrapper = ui.createElement("DIV", input, {
  3383. className: "filter-input-wrapper",
  3384. });
  3385.  
  3386. // 切换过滤模式
  3387. const switchMode = ui.createButton(this.settings.filterModes[0], () => {
  3388. const newMode = this.settings.switchModeByName(switchMode.innerText);
  3389.  
  3390. switchMode.innerText = newMode;
  3391. });
  3392.  
  3393. // 过滤标题
  3394. const switchTitle = ui.createElement("INPUT", [], {
  3395. type: "checkbox",
  3396. });
  3397.  
  3398. // 过滤内容
  3399. const switchContent = ui.createElement("INPUT", [], {
  3400. type: "checkbox",
  3401. });
  3402.  
  3403. // 过滤昵称
  3404. const switchUsername = ui.createElement("INPUT", [], {
  3405. type: "checkbox",
  3406. });
  3407.  
  3408. // 操作
  3409. const buttons = (() => {
  3410. const save = ui.createButton("添加", (e) => {
  3411. const entity = this.add(
  3412. input.value,
  3413. switchMode.innerText,
  3414. this.boolsToBinary(
  3415. switchTitle.checked,
  3416. switchContent.checked,
  3417. switchUsername.checked
  3418. )
  3419. );
  3420.  
  3421. table.update(e, ...this.column(entity));
  3422.  
  3423. this.renderNewLine();
  3424. });
  3425.  
  3426. return ui.createButtonGroup(save);
  3427. })();
  3428.  
  3429. // 添加至列表
  3430. table.add(
  3431. inputWrapper,
  3432. switchMode,
  3433. switchTitle,
  3434. switchContent,
  3435. switchUsername,
  3436. buttons
  3437. );
  3438. }
  3439.  
  3440. /**
  3441. * 过滤
  3442. * @param {*} item 绑定的 nFilter
  3443. * @param {*} result 过滤结果
  3444. */
  3445. async filter(item, result) {
  3446. // 获取列表
  3447. const list = this.list;
  3448.  
  3449. // 跳过低于当前的过滤模式
  3450. const filtered = Object.values(list).filter(
  3451. (item) => this.settings.getModeByName(item.filterMode) > result.mode
  3452. );
  3453.  
  3454. // 没有则跳过
  3455. if (filtered.length === 0) {
  3456. return;
  3457. }
  3458.  
  3459. // 根据过滤模式依次判断
  3460. const sorted = Tools.sortBy(filtered, (item) =>
  3461. this.settings.getModeByName(item.filterMode)
  3462. );
  3463.  
  3464. for (let i = 0; i < sorted.length; i += 1) {
  3465. const { keyword, filterMode } = sorted[i];
  3466.  
  3467. // 兼容旧版本数据
  3468. // 过滤类型,为一个二进制数,0b1 - 过滤标题,0b10 - 过滤内容,0b100 - 过滤昵称
  3469. const filterType =
  3470. sorted[i].filterType !== undefined
  3471. ? sorted[i].filterType
  3472. : sorted[i].filterLevel > 0
  3473. ? 0b11
  3474. : 0b01;
  3475.  
  3476. // 过滤标题
  3477. if (filterType & 0b1) {
  3478. const { subject } = item;
  3479.  
  3480. const match = subject.match(keyword);
  3481.  
  3482. if (match) {
  3483. const mode = this.settings.getModeByName(filterMode);
  3484.  
  3485. // 更新过滤模式和原因
  3486. result.mode = mode;
  3487. result.reason = `关键字: ${match[0]}`;
  3488. return;
  3489. }
  3490. }
  3491.  
  3492. // 过滤内容
  3493. if (filterType & 0b10) {
  3494. // 如果没有内容,则请求
  3495. if (item.content === undefined) {
  3496. await this.getPostInfo(item);
  3497. }
  3498.  
  3499. const { content } = item;
  3500.  
  3501. if (content) {
  3502. const match = content.match(keyword);
  3503.  
  3504. if (match) {
  3505. const mode = this.settings.getModeByName(filterMode);
  3506.  
  3507. // 更新过滤模式和原因
  3508. result.mode = mode;
  3509. result.reason = `关键字: ${match[0]}`;
  3510. return;
  3511. }
  3512. }
  3513. }
  3514.  
  3515. // 过滤昵称
  3516. if (filterType & 0b100) {
  3517. const { username } = item;
  3518.  
  3519. if (username) {
  3520. const match = username.match(keyword);
  3521.  
  3522. if (match) {
  3523. const mode = this.settings.getModeByName(filterMode);
  3524.  
  3525. // 更新过滤模式和原因
  3526. result.mode = mode;
  3527. result.reason = `关键字: ${match[0]}`;
  3528. return;
  3529. }
  3530. }
  3531. }
  3532. }
  3533. }
  3534.  
  3535. /**
  3536. * 重新过滤
  3537. */
  3538. reFilter() {
  3539. // 实际上应该根据过滤模式来筛选要过滤的部分
  3540. this.data.forEach((item) => {
  3541. item.execute();
  3542. });
  3543. }
  3544. }
  3545.  
  3546. /**
  3547. * 属地模块
  3548. */
  3549. class LocationModule extends Module {
  3550. /**
  3551. * 模块名称
  3552. */
  3553. static name = "location";
  3554.  
  3555. /**
  3556. * 模块标签
  3557. */
  3558. static label = "属地";
  3559.  
  3560. /**
  3561. * 顺序
  3562. */
  3563. static order = 50;
  3564.  
  3565. /**
  3566. * 请求缓存
  3567. */
  3568. cache = {};
  3569.  
  3570. /**
  3571. * 获取列表
  3572. */
  3573. get list() {
  3574. return this.settings.locations;
  3575. }
  3576.  
  3577. /**
  3578. * 获取属地
  3579. * @param {Number} id 属地 ID
  3580. */
  3581. get(id) {
  3582. // 获取列表
  3583. const list = this.list;
  3584.  
  3585. // 如果存在,则返回信息
  3586. if (list[id]) {
  3587. return list[id];
  3588. }
  3589.  
  3590. return null;
  3591. }
  3592.  
  3593. /**
  3594. * 添加属地
  3595. * @param {String} keyword 关键字
  3596. * @param {String} filterMode 过滤模式
  3597. */
  3598. add(keyword, filterMode) {
  3599. // 获取列表
  3600. const list = this.list;
  3601.  
  3602. // ID 为最大值 + 1
  3603. const id = Math.max(...Object.keys(list), 0) + 1;
  3604.  
  3605. // 写入属地信息
  3606. list[id] = {
  3607. id,
  3608. keyword,
  3609. filterMode,
  3610. };
  3611.  
  3612. // 保存数据
  3613. this.settings.locations = list;
  3614.  
  3615. // 重新过滤
  3616. this.reFilter();
  3617.  
  3618. // 返回添加的属地
  3619. return list[id];
  3620. }
  3621.  
  3622. /**
  3623. * 编辑属地
  3624. * @param {Number} id 属地 ID
  3625. * @param {*} values 属地信息
  3626. */
  3627. update(id, values) {
  3628. // 获取列表
  3629. const list = this.list;
  3630.  
  3631. // 如果不存在则跳过
  3632. if (Object.hasOwn(list, id) === false) {
  3633. return null;
  3634. }
  3635.  
  3636. // 获取属地
  3637. const entity = list[id];
  3638.  
  3639. // 更新属地
  3640. Object.assign(entity, values);
  3641.  
  3642. // 保存数据
  3643. this.settings.locations = list;
  3644.  
  3645. // 重新过滤
  3646. this.reFilter();
  3647. }
  3648.  
  3649. /**
  3650. * 删除属地
  3651. * @param {Number} id 属地 ID
  3652. */
  3653. remove(id) {
  3654. // 获取列表
  3655. const list = this.list;
  3656.  
  3657. // 如果不存在则跳过
  3658. if (Object.hasOwn(list, id) === false) {
  3659. return null;
  3660. }
  3661.  
  3662. // 获取属地
  3663. const entity = list[id];
  3664.  
  3665. // 删除属地
  3666. delete list[id];
  3667.  
  3668. // 保存数据
  3669. this.settings.locations = list;
  3670.  
  3671. // 重新过滤
  3672. this.reFilter();
  3673.  
  3674. // 返回删除的属地
  3675. return entity;
  3676. }
  3677.  
  3678. /**
  3679. * 获取 IP 属地
  3680. * @param {*} item 绑定的 nFilter
  3681. */
  3682. async getIpLocation(item) {
  3683. const { uid } = item;
  3684.  
  3685. // 如果是匿名直接跳过
  3686. if (uid <= 0) {
  3687. return null;
  3688. }
  3689.  
  3690. // 如果已有缓存,直接返回
  3691. if (Object.hasOwn(this.cache, uid)) {
  3692. return this.cache[uid];
  3693. }
  3694.  
  3695. // 请求属地
  3696. const ipLocations = await this.api.getIpLocations(uid);
  3697.  
  3698. // 写入缓存
  3699. if (ipLocations.length > 0) {
  3700. this.cache[uid] = ipLocations[0].ipLoc;
  3701. }
  3702.  
  3703. // 返回结果
  3704. return null;
  3705. }
  3706.  
  3707. /**
  3708. * 表格列
  3709. * @returns {Array} 表格列集合
  3710. */
  3711. columns() {
  3712. return [
  3713. { label: "关键字" },
  3714. { label: "过滤模式", center: true, width: 1 },
  3715. { label: "操作", width: 1 },
  3716. ];
  3717. }
  3718.  
  3719. /**
  3720. * 表格项
  3721. * @param {*} item 标记信息
  3722. * @returns {Array} 表格项集合
  3723. */
  3724. column(item) {
  3725. const { ui } = this;
  3726. const { table } = this.views;
  3727. const { id, keyword, filterMode } = item;
  3728.  
  3729. // 关键字
  3730. const input = ui.createElement("INPUT", [], {
  3731. type: "text",
  3732. value: keyword,
  3733. });
  3734.  
  3735. const inputWrapper = ui.createElement("DIV", input, {
  3736. className: "filter-input-wrapper",
  3737. });
  3738.  
  3739. // 切换过滤模式
  3740. const switchMode = ui.createButton(
  3741. filterMode || this.settings.filterModes[0],
  3742. () => {
  3743. const newMode = this.settings.switchModeByName(switchMode.innerText);
  3744.  
  3745. switchMode.innerText = newMode;
  3746. }
  3747. );
  3748.  
  3749. // 操作
  3750. const buttons = (() => {
  3751. const save = ui.createButton("保存", () => {
  3752. this.update(id, {
  3753. keyword: input.value,
  3754. filterMode: switchMode.innerText,
  3755. });
  3756. });
  3757.  
  3758. const remove = ui.createButton("删除", (e) => {
  3759. ui.confirm().then(() => {
  3760. this.remove(id);
  3761.  
  3762. table.remove(e);
  3763. });
  3764. });
  3765.  
  3766. return ui.createButtonGroup(save, remove);
  3767. })();
  3768.  
  3769. return [inputWrapper, switchMode, buttons];
  3770. }
  3771.  
  3772. /**
  3773. * 初始化组件
  3774. */
  3775. initComponents() {
  3776. super.initComponents();
  3777.  
  3778. const { ui } = this;
  3779. const { tabs, content } = ui.views;
  3780.  
  3781. const table = ui.createTable(this.columns());
  3782.  
  3783. const tips = ui.createElement("DIV", TIPS.keyword, {
  3784. className: "silver",
  3785. });
  3786.  
  3787. const tab = ui.createTab(
  3788. tabs,
  3789. this.constructor.label,
  3790. this.constructor.order,
  3791. {
  3792. onclick: () => {
  3793. this.render(content);
  3794. },
  3795. }
  3796. );
  3797.  
  3798. Object.assign(this.views, {
  3799. tab,
  3800. table,
  3801. });
  3802.  
  3803. this.views.container.appendChild(table);
  3804. this.views.container.appendChild(tips);
  3805. }
  3806.  
  3807. /**
  3808. * 渲染
  3809. * @param {HTMLElement} container 容器
  3810. */
  3811. render(container) {
  3812. super.render(container);
  3813.  
  3814. const { table } = this.views;
  3815.  
  3816. if (table) {
  3817. const { add, clear } = table;
  3818.  
  3819. clear();
  3820.  
  3821. Object.values(this.list).forEach((item) => {
  3822. const column = this.column(item);
  3823.  
  3824. add(...column);
  3825. });
  3826.  
  3827. this.renderNewLine();
  3828. }
  3829. }
  3830.  
  3831. /**
  3832. * 渲染新行
  3833. */
  3834. renderNewLine() {
  3835. const { ui } = this;
  3836. const { table } = this.views;
  3837.  
  3838. // 关键字
  3839. const input = ui.createElement("INPUT", [], {
  3840. type: "text",
  3841. });
  3842.  
  3843. const inputWrapper = ui.createElement("DIV", input, {
  3844. className: "filter-input-wrapper",
  3845. });
  3846.  
  3847. // 切换过滤模式
  3848. const switchMode = ui.createButton(this.settings.filterModes[0], () => {
  3849. const newMode = this.settings.switchModeByName(switchMode.innerText);
  3850.  
  3851. switchMode.innerText = newMode;
  3852. });
  3853.  
  3854. // 操作
  3855. const buttons = (() => {
  3856. const save = ui.createButton("添加", (e) => {
  3857. const entity = this.add(input.value, switchMode.innerText);
  3858.  
  3859. table.update(e, ...this.column(entity));
  3860.  
  3861. this.renderNewLine();
  3862. });
  3863.  
  3864. return ui.createButtonGroup(save);
  3865. })();
  3866.  
  3867. // 添加至列表
  3868. table.add(inputWrapper, switchMode, buttons);
  3869. }
  3870.  
  3871. /**
  3872. * 过滤
  3873. * @param {*} item 绑定的 nFilter
  3874. * @param {*} result 过滤结果
  3875. */
  3876. async filter(item, result) {
  3877. // 获取列表
  3878. const list = this.list;
  3879.  
  3880. // 跳过低于当前的过滤模式
  3881. const filtered = Object.values(list).filter(
  3882. (item) => this.settings.getModeByName(item.filterMode) > result.mode
  3883. );
  3884.  
  3885. // 没有则跳过
  3886. if (filtered.length === 0) {
  3887. return;
  3888. }
  3889.  
  3890. // 获取当前属地
  3891. const location = await this.getIpLocation(item);
  3892.  
  3893. // 请求失败则跳过
  3894. if (location === null) {
  3895. return;
  3896. }
  3897.  
  3898. // 根据过滤模式依次判断
  3899. const sorted = Tools.sortBy(filtered, (item) =>
  3900. this.settings.getModeByName(item.filterMode)
  3901. );
  3902.  
  3903. for (let i = 0; i < sorted.length; i += 1) {
  3904. const { keyword, filterMode } = sorted[i];
  3905.  
  3906. const match = location.match(keyword);
  3907.  
  3908. if (match) {
  3909. const mode = this.settings.getModeByName(filterMode);
  3910.  
  3911. // 更新过滤模式和原因
  3912. result.mode = mode;
  3913. result.reason = `属地: ${match[0]}`;
  3914. return;
  3915. }
  3916. }
  3917. }
  3918.  
  3919. /**
  3920. * 重新过滤
  3921. */
  3922. reFilter() {
  3923. // 实际上应该根据过滤模式来筛选要过滤的部分
  3924. this.data.forEach((item) => {
  3925. item.execute();
  3926. });
  3927. }
  3928. }
  3929.  
  3930. /**
  3931. * 版面或合集模块
  3932. */
  3933. class ForumOrSubsetModule extends Module {
  3934. /**
  3935. * 模块名称
  3936. */
  3937. static name = "forumOrSubset";
  3938.  
  3939. /**
  3940. * 模块标签
  3941. */
  3942. static label = "版面/合集";
  3943.  
  3944. /**
  3945. * 顺序
  3946. */
  3947. static order = 60;
  3948.  
  3949. /**
  3950. * 请求缓存
  3951. */
  3952. cache = {};
  3953.  
  3954. /**
  3955. * 获取列表
  3956. */
  3957. get list() {
  3958. return this.settings.forumOrSubsets;
  3959. }
  3960.  
  3961. /**
  3962. * 获取版面或合集
  3963. * @param {Number} id ID
  3964. */
  3965. get(id) {
  3966. // 获取列表
  3967. const list = this.list;
  3968.  
  3969. // 如果存在,则返回信息
  3970. if (list[id]) {
  3971. return list[id];
  3972. }
  3973.  
  3974. return null;
  3975. }
  3976.  
  3977. /**
  3978. * 添加版面或合集
  3979. * @param {Number} value 版面或合集链接
  3980. * @param {String} filterMode 过滤模式
  3981. */
  3982. async add(value, filterMode) {
  3983. // 获取链接参数
  3984. const params = new URLSearchParams(value.split("?")[1]);
  3985.  
  3986. // 获取 FID
  3987. const fid = parseInt(params.get("fid"), 10);
  3988.  
  3989. // 获取 STID
  3990. const stid = parseInt(params.get("stid"), 10);
  3991.  
  3992. // 如果 FID 或 STID 不存在,则提示错误
  3993. if (fid === NaN && stid === NaN) {
  3994. alert("版面或合集ID有误");
  3995. return;
  3996. }
  3997.  
  3998. // 获取列表
  3999. const list = this.list;
  4000.  
  4001. // ID 为 FID 或者 t + STID
  4002. const id = fid ? fid : `t${stid}`;
  4003.  
  4004. // 如果版面或合集 ID 已存在,则提示错误
  4005. if (Object.hasOwn(list, id)) {
  4006. alert("已有相同版面或合集ID");
  4007. return;
  4008. }
  4009.  
  4010. // 请求版面或合集信息
  4011. const info = await (async () => {
  4012. if (fid) {
  4013. return await this.api.getForumInfo(fid);
  4014. }
  4015.  
  4016. if (stid) {
  4017. const postInfo = await this.api.getPostInfo(stid);
  4018.  
  4019. if (postInfo) {
  4020. return {
  4021. name: postInfo.subject,
  4022. };
  4023. }
  4024. }
  4025.  
  4026. return null;
  4027. })();
  4028.  
  4029. // 如果版面或合集不存在,则提示错误
  4030. if (info === null || info === undefined) {
  4031. alert("版面或合集ID有误");
  4032. return;
  4033. }
  4034.  
  4035. // 写入版面或合集信息
  4036. list[id] = {
  4037. fid,
  4038. stid,
  4039. name: info.name,
  4040. filterMode,
  4041. };
  4042.  
  4043. // 保存数据
  4044. this.settings.forumOrSubsets = list;
  4045.  
  4046. // 重新过滤
  4047. this.reFilter();
  4048.  
  4049. // 返回添加的版面或合集
  4050. return list[id];
  4051. }
  4052.  
  4053. /**
  4054. * 编辑版面或合集
  4055. * @param {Number} id ID
  4056. * @param {*} values 版面或合集信息
  4057. */
  4058. update(id, values) {
  4059. // 获取列表
  4060. const list = this.list;
  4061.  
  4062. // 如果不存在则跳过
  4063. if (Object.hasOwn(list, id) === false) {
  4064. return null;
  4065. }
  4066.  
  4067. // 获取版面或合集
  4068. const entity = list[id];
  4069.  
  4070. // 更新版面或合集
  4071. Object.assign(entity, values);
  4072.  
  4073. // 保存数据
  4074. this.settings.forumOrSubsets = list;
  4075.  
  4076. // 重新过滤
  4077. this.reFilter();
  4078. }
  4079.  
  4080. /**
  4081. * 删除版面或合集
  4082. * @param {Number} id ID
  4083. */
  4084. remove(id) {
  4085. // 获取列表
  4086. const list = this.list;
  4087.  
  4088. // 如果不存在则跳过
  4089. if (Object.hasOwn(list, id) === false) {
  4090. return null;
  4091. }
  4092.  
  4093. // 获取版面或合集
  4094. const entity = list[id];
  4095.  
  4096. // 删除版面或合集
  4097. delete list[id];
  4098.  
  4099. // 保存数据
  4100. this.settings.forumOrSubsets = list;
  4101.  
  4102. // 重新过滤
  4103. this.reFilter();
  4104.  
  4105. // 返回删除的版面或合集
  4106. return entity;
  4107. }
  4108.  
  4109. /**
  4110. * 格式化版面或合集
  4111. * @param {Number} fid 版面 ID
  4112. * @param {Number} stid 合集 ID
  4113. * @param {String} name 版面或合集名称
  4114. */
  4115. formatForumOrSubset(fid, stid, name) {
  4116. const { ui } = this;
  4117.  
  4118. return ui.createElement("A", `[${name}]`, {
  4119. className: "b nobr",
  4120. href: fid ? `/thread.php?fid=${fid}` : `/thread.php?stid=${stid}`,
  4121. });
  4122. }
  4123.  
  4124. /**
  4125. * 表格列
  4126. * @returns {Array} 表格列集合
  4127. */
  4128. columns() {
  4129. return [
  4130. { label: "版面/合集" },
  4131. { label: "过滤模式", center: true, width: 1 },
  4132. { label: "操作", width: 1 },
  4133. ];
  4134. }
  4135.  
  4136. /**
  4137. * 表格项
  4138. * @param {*} item 版面或合集信息
  4139. * @returns {Array} 表格项集合
  4140. */
  4141. column(item) {
  4142. const { ui } = this;
  4143. const { table } = this.views;
  4144. const { fid, stid, name, filterMode } = item;
  4145.  
  4146. // ID 为 FID 或者 t + STID
  4147. const id = fid ? fid : `t${stid}`;
  4148.  
  4149. // 版面或合集
  4150. const forum = this.formatForumOrSubset(fid, stid, name);
  4151.  
  4152. // 切换过滤模式
  4153. const switchMode = ui.createButton(filterMode || "隐藏", () => {
  4154. const newMode = this.settings.switchModeByName(switchMode.innerText);
  4155.  
  4156. switchMode.innerText = newMode;
  4157. });
  4158.  
  4159. // 操作
  4160. const buttons = (() => {
  4161. const save = ui.createButton("保存", () => {
  4162. this.update(id, {
  4163. filterMode: switchMode.innerText,
  4164. });
  4165. });
  4166.  
  4167. const remove = ui.createButton("删除", (e) => {
  4168. ui.confirm().then(() => {
  4169. this.remove(id);
  4170.  
  4171. table.remove(e);
  4172. });
  4173. });
  4174.  
  4175. return ui.createButtonGroup(save, remove);
  4176. })();
  4177.  
  4178. return [forum, switchMode, buttons];
  4179. }
  4180.  
  4181. /**
  4182. * 初始化组件
  4183. */
  4184. initComponents() {
  4185. super.initComponents();
  4186.  
  4187. const { ui } = this;
  4188. const { tabs, content } = ui.views;
  4189.  
  4190. const table = ui.createTable(this.columns());
  4191.  
  4192. const tips = ui.createElement("DIV", TIPS.forumOrSubset, {
  4193. className: "silver",
  4194. });
  4195.  
  4196. const tab = ui.createTab(
  4197. tabs,
  4198. this.constructor.label,
  4199. this.constructor.order,
  4200. {
  4201. onclick: () => {
  4202. this.render(content);
  4203. },
  4204. }
  4205. );
  4206.  
  4207. Object.assign(this.views, {
  4208. tab,
  4209. table,
  4210. });
  4211.  
  4212. this.views.container.appendChild(table);
  4213. this.views.container.appendChild(tips);
  4214. }
  4215.  
  4216. /**
  4217. * 渲染
  4218. * @param {HTMLElement} container 容器
  4219. */
  4220. render(container) {
  4221. super.render(container);
  4222.  
  4223. const { table } = this.views;
  4224.  
  4225. if (table) {
  4226. const { add, clear } = table;
  4227.  
  4228. clear();
  4229.  
  4230. Object.values(this.list).forEach((item) => {
  4231. const column = this.column(item);
  4232.  
  4233. add(...column);
  4234. });
  4235.  
  4236. this.renderNewLine();
  4237. }
  4238. }
  4239.  
  4240. /**
  4241. * 渲染新行
  4242. */
  4243. renderNewLine() {
  4244. const { ui } = this;
  4245. const { table } = this.views;
  4246.  
  4247. // 版面或合集 ID
  4248. const forumInput = ui.createElement("INPUT", [], {
  4249. type: "text",
  4250. });
  4251.  
  4252. const forumInputWrapper = ui.createElement("DIV", forumInput, {
  4253. className: "filter-input-wrapper",
  4254. });
  4255.  
  4256. // 切换过滤模式
  4257. const switchMode = ui.createButton("隐藏", () => {
  4258. const newMode = this.settings.switchModeByName(switchMode.innerText);
  4259.  
  4260. switchMode.innerText = newMode;
  4261. });
  4262.  
  4263. // 操作
  4264. const buttons = (() => {
  4265. const save = ui.createButton("添加", async (e) => {
  4266. const entity = await this.add(forumInput.value, switchMode.innerText);
  4267.  
  4268. if (entity) {
  4269. table.update(e, ...this.column(entity));
  4270.  
  4271. this.renderNewLine();
  4272. }
  4273. });
  4274.  
  4275. return ui.createButtonGroup(save);
  4276. })();
  4277.  
  4278. // 添加至列表
  4279. table.add(forumInputWrapper, switchMode, buttons);
  4280. }
  4281.  
  4282. /**
  4283. * 过滤
  4284. * @param {*} item 绑定的 nFilter
  4285. * @param {*} result 过滤结果
  4286. */
  4287. async filter(item, result) {
  4288. // 没有版面 ID 或主题杂项则跳过
  4289. if (item.fid === null && item.topicMisc.length === 0) {
  4290. return;
  4291. }
  4292.  
  4293. // 获取列表
  4294. const list = this.list;
  4295.  
  4296. // 跳过低于当前的过滤模式
  4297. const filtered = Object.values(list).filter(
  4298. (item) => this.settings.getModeByName(item.filterMode) > result.mode
  4299. );
  4300.  
  4301. // 没有则跳过
  4302. if (filtered.length === 0) {
  4303. return;
  4304. }
  4305.  
  4306. // 解析主题杂项
  4307. const { _SFID, _STID } = commonui.topicMiscVar.unpack(item.topicMisc);
  4308.  
  4309. // 根据过滤模式依次判断
  4310. const sorted = Tools.sortBy(filtered, (item) =>
  4311. this.settings.getModeByName(item.filterMode)
  4312. );
  4313.  
  4314. for (let i = 0; i < sorted.length; i += 1) {
  4315. const { fid, stid, name, filterMode } = sorted[i];
  4316.  
  4317. if (fid) {
  4318. if (fid === parseInt(item.fid, 10) || fid === _SFID) {
  4319. const mode = this.settings.getModeByName(filterMode);
  4320.  
  4321. // 更新过滤模式和原因
  4322. result.mode = mode;
  4323. result.reason = `版面: ${name}`;
  4324. return;
  4325. }
  4326. }
  4327.  
  4328. if (stid) {
  4329. if (stid === parseInt(item.tid, 10) || stid === _STID) {
  4330. const mode = this.settings.getModeByName(filterMode);
  4331.  
  4332. // 更新过滤模式和原因
  4333. result.mode = mode;
  4334. result.reason = `合集: ${name}`;
  4335. return;
  4336. }
  4337. }
  4338. }
  4339. }
  4340.  
  4341. /**
  4342. * 重新过滤
  4343. */
  4344. reFilter() {
  4345. // 实际上应该根据过滤模式来筛选要过滤的部分
  4346. this.data.forEach((item) => {
  4347. item.execute();
  4348. });
  4349. }
  4350. }
  4351.  
  4352. /**
  4353. * 猎巫模块
  4354. *
  4355. * 其实是通过 Cache 模块读取配置,而非 Settings
  4356. */
  4357. class HunterModule extends Module {
  4358. /**
  4359. * 模块名称
  4360. */
  4361. static name = "hunter";
  4362.  
  4363. /**
  4364. * 模块标签
  4365. */
  4366. static label = "猎巫";
  4367.  
  4368. /**
  4369. * 顺序
  4370. */
  4371. static order = 70;
  4372.  
  4373. /**
  4374. * 请求缓存
  4375. */
  4376. cache = {};
  4377.  
  4378. /**
  4379. * 请求队列
  4380. */
  4381. queue = [];
  4382.  
  4383. /**
  4384. * 获取列表
  4385. */
  4386. get list() {
  4387. return this.settings.cache
  4388. .get("WITCH_HUNT")
  4389. .then((values) => values || []);
  4390. }
  4391.  
  4392. /**
  4393. * 获取猎巫
  4394. * @param {Number} fid 版面 ID
  4395. */
  4396. async get(fid) {
  4397. // 获取列表
  4398. const list = await this.list;
  4399.  
  4400. // 如果存在,则返回信息
  4401. if (list[fid]) {
  4402. return list[fid];
  4403. }
  4404.  
  4405. return null;
  4406. }
  4407.  
  4408. /**
  4409. * 添加猎巫
  4410. * @param {Number} fid 版面 ID
  4411. * @param {String} label 标签
  4412. * @param {String} filterMode 过滤模式
  4413. * @param {Number} filterLevel 过滤等级: 0 - 仅标记; 1 - 标记并过滤
  4414. */
  4415. async add(fid, label, filterMode, filterLevel) {
  4416. // FID 只能是数字
  4417. fid = parseInt(fid, 10);
  4418.  
  4419. // 获取列表
  4420. const list = await this.list;
  4421.  
  4422. // 如果版面 ID 已存在,则提示错误
  4423. if (Object.keys(list).includes(fid)) {
  4424. alert("已有相同版面ID");
  4425. return;
  4426. }
  4427.  
  4428. // 请求版面信息
  4429. const info = await this.api.getForumInfo(fid);
  4430.  
  4431. // 如果版面不存在,则提示错误
  4432. if (info === null || info === undefined) {
  4433. alert("版面ID有误");
  4434. return;
  4435. }
  4436.  
  4437. // 计算标记颜色
  4438. const color = Tools.generateColor(info.name);
  4439.  
  4440. // 写入猎巫信息
  4441. list[fid] = {
  4442. fid,
  4443. name: info.name,
  4444. label,
  4445. color,
  4446. filterMode,
  4447. filterLevel,
  4448. };
  4449.  
  4450. // 保存数据
  4451. this.settings.cache.put("WITCH_HUNT", list);
  4452.  
  4453. // 重新过滤
  4454. this.reFilter(true);
  4455.  
  4456. // 返回添加的猎巫
  4457. return list[fid];
  4458. }
  4459.  
  4460. /**
  4461. * 编辑猎巫
  4462. * @param {Number} fid 版面 ID
  4463. * @param {*} values 猎巫信息
  4464. */
  4465. async update(fid, values) {
  4466. // 获取列表
  4467. const list = await this.list;
  4468.  
  4469. // 如果不存在则跳过
  4470. if (Object.hasOwn(list, fid) === false) {
  4471. return null;
  4472. }
  4473.  
  4474. // 获取猎巫
  4475. const entity = list[fid];
  4476.  
  4477. // 更新猎巫
  4478. Object.assign(entity, values);
  4479.  
  4480. // 保存数据
  4481. this.settings.cache.put("WITCH_HUNT", list);
  4482.  
  4483. // 重新过滤,更新样式即可
  4484. this.reFilter(false);
  4485. }
  4486.  
  4487. /**
  4488. * 删除猎巫
  4489. * @param {Number} fid 版面 ID
  4490. */
  4491. async remove(fid) {
  4492. // 获取列表
  4493. const list = await this.list;
  4494.  
  4495. // 如果不存在则跳过
  4496. if (Object.hasOwn(list, fid) === false) {
  4497. return null;
  4498. }
  4499.  
  4500. // 获取猎巫
  4501. const entity = list[fid];
  4502.  
  4503. // 删除猎巫
  4504. delete list[fid];
  4505.  
  4506. // 保存数据
  4507. this.settings.cache.put("WITCH_HUNT", list);
  4508.  
  4509. // 重新过滤
  4510. this.reFilter(true);
  4511.  
  4512. // 返回删除的猎巫
  4513. return entity;
  4514. }
  4515.  
  4516. /**
  4517. * 格式化版面
  4518. * @param {Number} fid 版面 ID
  4519. * @param {String} name 版面名称
  4520. */
  4521. formatForum(fid, name) {
  4522. const { ui } = this;
  4523.  
  4524. return ui.createElement("A", `[${name}]`, {
  4525. className: "b nobr",
  4526. href: `/thread.php?fid=${fid}`,
  4527. });
  4528. }
  4529.  
  4530. /**
  4531. * 格式化标签
  4532. * @param {String} name 标签名称
  4533. * @param {String} name 标签颜色
  4534. */
  4535. formatLabel(name, color) {
  4536. const { ui } = this;
  4537.  
  4538. return ui.createElement("B", name, {
  4539. className: "block_txt nobr",
  4540. style: `background: ${color}; color: #FFF; margin: 0.1em 0.2em;`,
  4541. });
  4542. }
  4543.  
  4544. /**
  4545. * 表格列
  4546. * @returns {Array} 表格列集合
  4547. */
  4548. columns() {
  4549. return [
  4550. { label: "版面", width: 200 },
  4551. { label: "标签" },
  4552. { label: "启用过滤", center: true, width: 1 },
  4553. { label: "过滤模式", center: true, width: 1 },
  4554. { label: "操作", width: 1 },
  4555. ];
  4556. }
  4557.  
  4558. /**
  4559. * 表格项
  4560. * @param {*} item 猎巫信息
  4561. * @returns {Array} 表格项集合
  4562. */
  4563. column(item) {
  4564. const { ui } = this;
  4565. const { table } = this.views;
  4566. const { fid, name, label, color, filterMode, filterLevel } = item;
  4567.  
  4568. // 版面
  4569. const forum = this.formatForum(fid, name);
  4570.  
  4571. // 标签
  4572. const labelElement = this.formatLabel(label, color);
  4573.  
  4574. // 启用过滤
  4575. const switchLevel = ui.createElement("INPUT", [], {
  4576. type: "checkbox",
  4577. checked: filterLevel > 0,
  4578. });
  4579.  
  4580. // 切换过滤模式
  4581. const switchMode = ui.createButton(
  4582. filterMode || this.settings.filterModes[0],
  4583. () => {
  4584. const newMode = this.settings.switchModeByName(switchMode.innerText);
  4585.  
  4586. switchMode.innerText = newMode;
  4587. }
  4588. );
  4589.  
  4590. // 操作
  4591. const buttons = (() => {
  4592. const save = ui.createButton("保存", () => {
  4593. this.update(fid, {
  4594. filterMode: switchMode.innerText,
  4595. filterLevel: switchLevel.checked ? 1 : 0,
  4596. });
  4597. });
  4598.  
  4599. const remove = ui.createButton("删除", (e) => {
  4600. ui.confirm().then(async () => {
  4601. await this.remove(fid);
  4602.  
  4603. table.remove(e);
  4604. });
  4605. });
  4606.  
  4607. return ui.createButtonGroup(save, remove);
  4608. })();
  4609.  
  4610. return [forum, labelElement, switchLevel, switchMode, buttons];
  4611. }
  4612.  
  4613. /**
  4614. * 初始化组件
  4615. */
  4616. initComponents() {
  4617. super.initComponents();
  4618.  
  4619. const { ui } = this;
  4620. const { tabs, content } = ui.views;
  4621.  
  4622. const table = ui.createTable(this.columns());
  4623.  
  4624. const tips = ui.createElement(
  4625. "DIV",
  4626. [TIPS.hunter, TIPS.error].join("<br/>"),
  4627. {
  4628. className: "silver",
  4629. }
  4630. );
  4631.  
  4632. const tab = ui.createTab(
  4633. tabs,
  4634. this.constructor.label,
  4635. this.constructor.order,
  4636. {
  4637. onclick: () => {
  4638. this.render(content);
  4639. },
  4640. }
  4641. );
  4642.  
  4643. Object.assign(this.views, {
  4644. tab,
  4645. table,
  4646. });
  4647.  
  4648. this.views.container.appendChild(table);
  4649. this.views.container.appendChild(tips);
  4650. }
  4651.  
  4652. /**
  4653. * 渲染
  4654. * @param {HTMLElement} container 容器
  4655. */
  4656. render(container) {
  4657. super.render(container);
  4658.  
  4659. const { table } = this.views;
  4660.  
  4661. if (table) {
  4662. const { add, clear } = table;
  4663.  
  4664. clear();
  4665.  
  4666. this.list.then((values) => {
  4667. Object.values(values).forEach((item) => {
  4668. const column = this.column(item);
  4669.  
  4670. add(...column);
  4671. });
  4672.  
  4673. this.renderNewLine();
  4674. });
  4675. }
  4676. }
  4677.  
  4678. /**
  4679. * 渲染新行
  4680. */
  4681. renderNewLine() {
  4682. const { ui } = this;
  4683. const { table } = this.views;
  4684.  
  4685. // 版面 ID
  4686. const forumInput = ui.createElement("INPUT", [], {
  4687. type: "text",
  4688. });
  4689.  
  4690. const forumInputWrapper = ui.createElement("DIV", forumInput, {
  4691. className: "filter-input-wrapper",
  4692. });
  4693.  
  4694. // 标签
  4695. const labelInput = ui.createElement("INPUT", [], {
  4696. type: "text",
  4697. });
  4698.  
  4699. const labelInputWrapper = ui.createElement("DIV", labelInput, {
  4700. className: "filter-input-wrapper",
  4701. });
  4702.  
  4703. // 启用过滤
  4704. const switchLevel = ui.createElement("INPUT", [], {
  4705. type: "checkbox",
  4706. });
  4707.  
  4708. // 切换过滤模式
  4709. const switchMode = ui.createButton(this.settings.filterModes[0], () => {
  4710. const newMode = this.settings.switchModeByName(switchMode.innerText);
  4711.  
  4712. switchMode.innerText = newMode;
  4713. });
  4714.  
  4715. // 操作
  4716. const buttons = (() => {
  4717. const save = ui.createButton("添加", async (e) => {
  4718. const entity = await this.add(
  4719. forumInput.value,
  4720. labelInput.value,
  4721. switchMode.innerText,
  4722. switchLevel.checked ? 1 : 0
  4723. );
  4724.  
  4725. if (entity) {
  4726. table.update(e, ...this.column(entity));
  4727.  
  4728. this.renderNewLine();
  4729. }
  4730. });
  4731.  
  4732. return ui.createButtonGroup(save);
  4733. })();
  4734.  
  4735. // 添加至列表
  4736. table.add(
  4737. forumInputWrapper,
  4738. labelInputWrapper,
  4739. switchLevel,
  4740. switchMode,
  4741. buttons
  4742. );
  4743. }
  4744.  
  4745. /**
  4746. * 过滤
  4747. * @param {*} item 绑定的 nFilter
  4748. * @param {*} result 过滤结果
  4749. */
  4750. async filter(item, result) {
  4751. // 获取当前猎巫结果
  4752. const hunter = item.hunter || [];
  4753.  
  4754. // 如果没有猎巫结果,则跳过
  4755. if (hunter.length === 0) {
  4756. return;
  4757. }
  4758.  
  4759. // 获取列表
  4760. const items = await this.list;
  4761.  
  4762. // 筛选出匹配的猎巫
  4763. const list = Object.values(items).filter(({ fid }) =>
  4764. hunter.includes(fid)
  4765. );
  4766.  
  4767. // 取最高的过滤模式
  4768. // 低于当前的过滤模式则跳过
  4769. let max = result.mode;
  4770. let res = null;
  4771.  
  4772. for (const entity of list) {
  4773. const { filterLevel, filterMode } = entity;
  4774.  
  4775. // 仅标记
  4776. if (filterLevel === 0) {
  4777. continue;
  4778. }
  4779.  
  4780. // 获取过滤模式
  4781. const mode = this.settings.getModeByName(filterMode);
  4782.  
  4783. if (mode <= max) {
  4784. continue;
  4785. }
  4786.  
  4787. max = mode;
  4788. res = entity;
  4789. }
  4790.  
  4791. // 没有匹配的则跳过
  4792. if (res === null) {
  4793. return;
  4794. }
  4795.  
  4796. // 更新过滤模式和原因
  4797. result.mode = max;
  4798. result.reason = `猎巫: ${res.label}`;
  4799. }
  4800.  
  4801. /**
  4802. * 通知
  4803. * @param {*} item 绑定的 nFilter
  4804. */
  4805. async notify(item) {
  4806. const { uid, tags } = item;
  4807.  
  4808. // 如果没有 tags 组件则跳过
  4809. if (tags === null) {
  4810. return;
  4811. }
  4812.  
  4813. // 如果是匿名,隐藏组件
  4814. if (uid <= 0) {
  4815. tags.style.display = "none";
  4816. return;
  4817. }
  4818.  
  4819. // 删除旧标签
  4820. [...tags.querySelectorAll("[fid]")].forEach((item) => {
  4821. tags.removeChild(item);
  4822. });
  4823.  
  4824. // 如果没有请求,开始请求
  4825. if (Object.hasOwn(item, "hunter") === false) {
  4826. this.execute(item);
  4827. return;
  4828. }
  4829.  
  4830. // 获取当前猎巫结果
  4831. const hunter = item.hunter;
  4832.  
  4833. // 如果没有猎巫结果,则跳过
  4834. if (hunter.length === 0) {
  4835. return;
  4836. }
  4837.  
  4838. // 格式化标签
  4839. const items = await Promise.all(
  4840. hunter.map(async (fid) => {
  4841. const item = await this.get(fid);
  4842.  
  4843. if (item) {
  4844. const element = this.formatLabel(item.label, item.color);
  4845.  
  4846. element.setAttribute("fid", fid);
  4847.  
  4848. return element;
  4849. }
  4850.  
  4851. return null;
  4852. })
  4853. );
  4854.  
  4855. // 加入组件
  4856. items.forEach((item) => {
  4857. if (item) {
  4858. tags.appendChild(item);
  4859. }
  4860. });
  4861. }
  4862.  
  4863. /**
  4864. * 重新过滤
  4865. * @param {Boolean} clear 是否清除缓存
  4866. */
  4867. reFilter(clear) {
  4868. // 清除缓存
  4869. if (clear) {
  4870. this.cache = {};
  4871. }
  4872.  
  4873. // 重新过滤
  4874. this.data.forEach((item) => {
  4875. // 不需要清除缓存的话,只要重新加载标记
  4876. if (clear === false) {
  4877. item.hunter = [];
  4878. }
  4879.  
  4880. // 重新猎巫
  4881. this.execute(item);
  4882. });
  4883. }
  4884.  
  4885. /**
  4886. * 猎巫
  4887. * @param {*} item 绑定的 nFilter
  4888. */
  4889. async execute(item) {
  4890. const { uid } = item;
  4891. const { api, cache, queue, list } = this;
  4892.  
  4893. // 如果是匿名,则跳过
  4894. if (uid <= 0) {
  4895. return;
  4896. }
  4897.  
  4898. // 初始化猎巫结果,用于标识正在猎巫
  4899. item.hunter = item.hunter || [];
  4900.  
  4901. // 获取列表
  4902. const items = await list;
  4903.  
  4904. // 没有设置且没有旧数据,直接跳过
  4905. if (items.length === 0 && item.hunter.length === 0) {
  4906. return;
  4907. }
  4908.  
  4909. // 重新过滤
  4910. const reload = (newValue) => {
  4911. const isEqual = newValue.sort().join() === item.hunter.sort().join();
  4912.  
  4913. if (isEqual) {
  4914. return;
  4915. }
  4916.  
  4917. item.hunter = newValue;
  4918. item.execute();
  4919. };
  4920.  
  4921. // 创建任务
  4922. const task = async () => {
  4923. // 如果缓存里没有记录,请求数据并写入缓存
  4924. if (Object.hasOwn(cache, uid) === false) {
  4925. cache[uid] = [];
  4926.  
  4927. await Promise.all(
  4928. Object.keys(items).map(async (fid) => {
  4929. // 转换为数字格式
  4930. const id = parseInt(fid, 10);
  4931.  
  4932. // 当前版面发言记录
  4933. const result = await api.getForumPosted(id, uid);
  4934.  
  4935. // 写入当前设置
  4936. if (result) {
  4937. cache[uid].push(id);
  4938. }
  4939. })
  4940. );
  4941. }
  4942.  
  4943. // 重新过滤
  4944. reload(cache[uid]);
  4945.  
  4946. // 将当前任务移出队列
  4947. queue.shift();
  4948.  
  4949. // 如果还有任务,继续执行
  4950. if (queue.length > 0) {
  4951. queue[0]();
  4952. }
  4953. };
  4954.  
  4955. // 队列里已经有任务
  4956. const isRunning = queue.length > 0;
  4957.  
  4958. // 加入队列
  4959. queue.push(task);
  4960.  
  4961. // 如果没有正在执行的任务,则立即执行
  4962. if (isRunning === false) {
  4963. task();
  4964. }
  4965. }
  4966. }
  4967.  
  4968. /**
  4969. * 杂项模块
  4970. */
  4971. class MiscModule extends Module {
  4972. /**
  4973. * 模块名称
  4974. */
  4975. static name = "misc";
  4976.  
  4977. /**
  4978. * 模块标签
  4979. */
  4980. static label = "杂项";
  4981.  
  4982. /**
  4983. * 顺序
  4984. */
  4985. static order = 80;
  4986.  
  4987. /**
  4988. * 请求缓存
  4989. */
  4990. cache = {
  4991. topicNums: {},
  4992. topicPerPages: {},
  4993. };
  4994.  
  4995. /**
  4996. * 获取用户信息(从页面上)
  4997. * @param {*} item 绑定的 nFilter
  4998. */
  4999. getUserInfo(item) {
  5000. const { uid } = item;
  5001.  
  5002. // 如果是匿名直接跳过
  5003. if (uid <= 0) {
  5004. return;
  5005. }
  5006.  
  5007. // 回复页面可以直接获取到用户信息和声望
  5008. if (commonui.userInfo) {
  5009. // 取得用户信息
  5010. const userInfo = commonui.userInfo.users[uid];
  5011.  
  5012. // 绑定用户信息和声望
  5013. if (userInfo) {
  5014. item.userInfo = userInfo;
  5015. item.username = userInfo.username;
  5016.  
  5017. item.reputation = (() => {
  5018. const reputations = commonui.userInfo.reputations;
  5019.  
  5020. if (reputations) {
  5021. for (let fid in reputations) {
  5022. return reputations[fid][uid] || 0;
  5023. }
  5024. }
  5025.  
  5026. return NaN;
  5027. })();
  5028. }
  5029. }
  5030. }
  5031.  
  5032. /**
  5033. * 获取帖子数据
  5034. * @param {*} item 绑定的 nFilter
  5035. */
  5036. async getPostInfo(item) {
  5037. const { tid, pid } = item;
  5038.  
  5039. // 请求帖子数据
  5040. const { subject, content, userInfo, reputation } =
  5041. await this.api.getPostInfo(tid, pid);
  5042.  
  5043. // 绑定用户信息和声望
  5044. if (userInfo) {
  5045. item.userInfo = userInfo;
  5046. item.username = userInfo.username;
  5047. item.reputation = reputation;
  5048. }
  5049.  
  5050. // 绑定标题和内容
  5051. item.subject = subject;
  5052. item.content = content;
  5053. }
  5054.  
  5055. /**
  5056. * 获取主题数量
  5057. * @param {*} item 绑定的 nFilter
  5058. */
  5059. async getTopicNum(item) {
  5060. const { uid } = item;
  5061.  
  5062. // 如果是匿名直接跳过
  5063. if (uid <= 0) {
  5064. return;
  5065. }
  5066.  
  5067. // 如果已有缓存,直接返回
  5068. if (Object.hasOwn(this.cache.topicNums, uid)) {
  5069. return this.cache.topicNums[uid];
  5070. }
  5071.  
  5072. // 请求数量
  5073. const number = await this.api.getTopicNum(uid);
  5074.  
  5075. // 写入缓存
  5076. this.cache.topicNums[uid] = number;
  5077.  
  5078. // 返回结果
  5079. return number;
  5080. }
  5081.  
  5082. /**
  5083. * 初始化组件
  5084. */
  5085. initComponents() {
  5086. super.initComponents();
  5087.  
  5088. const { settings, ui } = this;
  5089. const { tabs, content } = ui.views;
  5090.  
  5091. const tab = ui.createTab(
  5092. tabs,
  5093. this.constructor.label,
  5094. this.constructor.order,
  5095. {
  5096. onclick: () => {
  5097. this.render(content);
  5098. },
  5099. }
  5100. );
  5101.  
  5102. Object.assign(this.views, {
  5103. tab,
  5104. });
  5105.  
  5106. const add = (order, ...elements) => {
  5107. this.views.container.appendChild(
  5108. ui.createElement("DIV", [...elements, ui.createElement("BR", [])], {
  5109. order,
  5110. })
  5111. );
  5112. };
  5113.  
  5114. // 小号过滤(注册时间)
  5115. {
  5116. const input = ui.createElement("INPUT", [], {
  5117. type: "text",
  5118. value: settings.filterRegdateLimit / 86400000,
  5119. maxLength: 4,
  5120. style: "width: 48px;",
  5121. });
  5122.  
  5123. const button = ui.createButton("确认", () => {
  5124. const newValue = (() => {
  5125. const result = parseInt(input.value, 10);
  5126.  
  5127. if (result > 0) {
  5128. return result;
  5129. }
  5130.  
  5131. return 0;
  5132. })();
  5133.  
  5134. settings.filterRegdateLimit = newValue * 86400000;
  5135.  
  5136. input.value = newValue;
  5137.  
  5138. this.reFilter();
  5139. });
  5140.  
  5141. const element = ui.createElement("DIV", [
  5142. "隐藏注册时间小于",
  5143. input,
  5144. "天的用户",
  5145. button,
  5146. ]);
  5147.  
  5148. add(this.constructor.order + 0, element);
  5149. }
  5150.  
  5151. // 小号过滤(发帖数)
  5152. {
  5153. const input = ui.createElement("INPUT", [], {
  5154. type: "text",
  5155. value: settings.filterPostnumLimit,
  5156. maxLength: 5,
  5157. style: "width: 48px;",
  5158. });
  5159.  
  5160. const button = ui.createButton("确认", () => {
  5161. const newValue = (() => {
  5162. const result = parseInt(input.value, 10);
  5163.  
  5164. if (result > 0) {
  5165. return result;
  5166. }
  5167.  
  5168. return 0;
  5169. })();
  5170.  
  5171. settings.filterPostnumLimit = newValue;
  5172.  
  5173. input.value = newValue;
  5174.  
  5175. this.reFilter();
  5176. });
  5177.  
  5178. const element = ui.createElement("DIV", [
  5179. "隐藏发帖数量小于",
  5180. input,
  5181. "贴的用户",
  5182. button,
  5183. ]);
  5184.  
  5185. add(this.constructor.order + 1, element);
  5186. }
  5187.  
  5188. // 流量号过滤(主题比例)
  5189. {
  5190. const input = ui.createElement("INPUT", [], {
  5191. type: "text",
  5192. value: settings.filterTopicRateLimit,
  5193. maxLength: 3,
  5194. style: "width: 48px;",
  5195. });
  5196.  
  5197. const button = ui.createButton("确认", () => {
  5198. const newValue = (() => {
  5199. const result = parseInt(input.value, 10);
  5200.  
  5201. if (result > 0 && result <= 100) {
  5202. return result;
  5203. }
  5204.  
  5205. return 100;
  5206. })();
  5207.  
  5208. settings.filterTopicRateLimit = newValue;
  5209.  
  5210. input.value = newValue;
  5211.  
  5212. this.reFilter();
  5213. });
  5214.  
  5215. const element = ui.createElement("DIV", [
  5216. "隐藏发帖比例大于",
  5217. input,
  5218. "%的用户",
  5219. button,
  5220. ]);
  5221.  
  5222. const tips = ui.createElement("DIV", TIPS.error, {
  5223. className: "silver",
  5224. });
  5225.  
  5226. add(
  5227. this.constructor.order + 2,
  5228. ui.createElement("DIV", [element, tips])
  5229. );
  5230. }
  5231.  
  5232. // 流量号过滤(每页主题数量)
  5233. {
  5234. const input = ui.createElement("INPUT", [], {
  5235. type: "text",
  5236. value: settings.filterTopicPerPageLimit || "",
  5237. maxLength: 3,
  5238. style: "width: 48px;",
  5239. });
  5240.  
  5241. const button = ui.createButton("确认", () => {
  5242. const newValue = (() => {
  5243. const result = parseInt(input.value, 10);
  5244.  
  5245. if (result > 0) {
  5246. return result;
  5247. }
  5248.  
  5249. return NaN;
  5250. })();
  5251.  
  5252. settings.filterTopicPerPageLimit = newValue;
  5253.  
  5254. input.value = newValue || "";
  5255.  
  5256. this.reFilter();
  5257. });
  5258.  
  5259. const element = ui.createElement("DIV", [
  5260. "隐藏每页主题大于",
  5261. input,
  5262. "贴的用户",
  5263. button,
  5264. ]);
  5265.  
  5266. const tips = ui.createElement("DIV", TIPS.filterTopicPerPage, {
  5267. className: "silver",
  5268. });
  5269.  
  5270. add(
  5271. this.constructor.order + 3,
  5272. ui.createElement("DIV", [element, tips])
  5273. );
  5274. }
  5275.  
  5276. // 声望过滤
  5277. {
  5278. const input = ui.createElement("INPUT", [], {
  5279. type: "text",
  5280. value: settings.filterReputationLimit || "",
  5281. maxLength: 4,
  5282. style: "width: 48px;",
  5283. });
  5284.  
  5285. const button = ui.createButton("确认", () => {
  5286. const newValue = parseInt(input.value, 10);
  5287.  
  5288. settings.filterReputationLimit = newValue;
  5289.  
  5290. input.value = newValue || "";
  5291.  
  5292. this.reFilter();
  5293. });
  5294.  
  5295. const element = ui.createElement("DIV", [
  5296. "隐藏版面声望低于",
  5297. input,
  5298. "点的用户",
  5299. button,
  5300. ]);
  5301.  
  5302. add(this.constructor.order + 4, element);
  5303. }
  5304.  
  5305. // 匿名过滤
  5306. {
  5307. const input = ui.createElement("INPUT", [], {
  5308. type: "checkbox",
  5309. checked: settings.filterAnonymous,
  5310. });
  5311.  
  5312. const label = ui.createElement("LABEL", ["隐藏匿名的用户", input], {
  5313. style: "display: flex;",
  5314. });
  5315.  
  5316. const element = ui.createElement("DIV", label);
  5317.  
  5318. input.onchange = () => {
  5319. settings.filterAnonymous = input.checked;
  5320.  
  5321. this.reFilter();
  5322. };
  5323.  
  5324. add(this.constructor.order + 5, element);
  5325. }
  5326.  
  5327. // 缩略图过滤
  5328. {
  5329. const items = {
  5330. none: "禁用",
  5331. all: "启用",
  5332. thumbnail: "仅图片",
  5333. };
  5334.  
  5335. const select = ui.createElement(
  5336. "SELECT",
  5337. Object.keys(items).map((value) =>
  5338. ui.createElement("OPTION", items[value], { value })
  5339. ),
  5340. {
  5341. value: settings.filterThumbnail || "none",
  5342. }
  5343. );
  5344.  
  5345. const label = ui.createElement(
  5346. "LABEL",
  5347. ["隐藏含缩略图的帖子", select],
  5348. {
  5349. style: "display: flex;",
  5350. }
  5351. );
  5352.  
  5353. const element = ui.createElement("DIV", label);
  5354.  
  5355. select.onchange = () => {
  5356. settings.filterThumbnail = select.value;
  5357.  
  5358. this.reFilter();
  5359. };
  5360.  
  5361. add(this.constructor.order + 6, element);
  5362. }
  5363. }
  5364.  
  5365. /**
  5366. * 过滤
  5367. * @param {*} item 绑定的 nFilter
  5368. * @param {*} result 过滤结果
  5369. */
  5370. async filter(item, result) {
  5371. // 获取隐藏模式下标
  5372. const mode = this.settings.getModeByName("隐藏");
  5373.  
  5374. // 如果当前模式不低于隐藏模式,则跳过
  5375. if (result.mode >= mode) {
  5376. return;
  5377. }
  5378.  
  5379. // 缩略图过滤
  5380. await this.filterByThumbnail(item, result);
  5381.  
  5382. // 匿名过滤
  5383. await this.filterByAnonymous(item, result);
  5384.  
  5385. // 注册时间过滤
  5386. await this.filterByRegdate(item, result);
  5387.  
  5388. // 发帖数量过滤
  5389. await this.filterByPostnum(item, result);
  5390.  
  5391. // 发帖比例过滤
  5392. await this.filterByTopicRate(item, result);
  5393.  
  5394. // 每页主题数量过滤
  5395. await this.filterByTopicPerPage(item, result);
  5396.  
  5397. // 版面声望过滤
  5398. await this.filterByReputation(item, result);
  5399. }
  5400.  
  5401. /**
  5402. * 根据缩略图过滤
  5403. * @param {*} item 绑定的 nFilter
  5404. * @param {*} result 过滤结果
  5405. */
  5406. async filterByThumbnail(item, result) {
  5407. const { tid, title } = item;
  5408.  
  5409. // 获取隐藏模式下标
  5410. const mode = this.settings.getModeByName("隐藏");
  5411.  
  5412. // 如果当前模式不低于隐藏模式,则跳过
  5413. if (result.mode >= mode) {
  5414. return;
  5415. }
  5416.  
  5417. // 找到对应数据
  5418. const data = topicModule.data.find((item) => item[8] === tid);
  5419.  
  5420. // 如果不含缩略图,则跳过
  5421. if (data[20] === null) {
  5422. return;
  5423. }
  5424.  
  5425. // 调整缩略图
  5426. const handleThumbnail = () => {
  5427. // 获取过滤缩略图设置
  5428. const filterThumbnail = this.settings.filterThumbnail || "none";
  5429.  
  5430. // 获取容器
  5431. const container = title.parentNode;
  5432.  
  5433. // 定位锚点
  5434. const anchor = (() => {
  5435. const img = container.querySelector("IMG");
  5436.  
  5437. if (img) {
  5438. return img.closest("DIV");
  5439. }
  5440.  
  5441. return null;
  5442. })();
  5443.  
  5444. // 如果没有锚点,则增加观察
  5445. if (anchor === null) {
  5446. const observer = new MutationObserver(handleThumbnail);
  5447.  
  5448. observer.observe(container, {
  5449. childList: true,
  5450. });
  5451. } else if (filterThumbnail === "none") {
  5452. anchor.style.removeProperty("display");
  5453. } else {
  5454. anchor.style.display = "none";
  5455. }
  5456.  
  5457. return filterThumbnail;
  5458. };
  5459.  
  5460. // 更新过滤模式和原因
  5461. if (handleThumbnail() === "all") {
  5462. result.mode = mode;
  5463. result.reason = "缩略图";
  5464. }
  5465. }
  5466.  
  5467. /**
  5468. * 根据匿名过滤
  5469. * @param {*} item 绑定的 nFilter
  5470. * @param {*} result 过滤结果
  5471. */
  5472. async filterByAnonymous(item, result) {
  5473. const { uid, username } = item;
  5474.  
  5475. // 如果不是匿名,则跳过
  5476. if (uid > 0) {
  5477. return;
  5478. }
  5479.  
  5480. // 如果没有引用,则跳过
  5481. if (username === undefined) {
  5482. return;
  5483. }
  5484.  
  5485. // 获取隐藏模式下标
  5486. const mode = this.settings.getModeByName("隐藏");
  5487.  
  5488. // 如果当前模式不低于隐藏模式,则跳过
  5489. if (result.mode >= mode) {
  5490. return;
  5491. }
  5492.  
  5493. // 获取过滤匿名设置
  5494. const filterAnonymous = this.settings.filterAnonymous;
  5495.  
  5496. if (filterAnonymous) {
  5497. // 更新过滤模式和原因
  5498. result.mode = mode;
  5499. result.reason = "匿名";
  5500. }
  5501. }
  5502.  
  5503. /**
  5504. * 根据注册时间过滤
  5505. * @param {*} item 绑定的 nFilter
  5506. * @param {*} result 过滤结果
  5507. */
  5508. async filterByRegdate(item, result) {
  5509. const { uid } = item;
  5510.  
  5511. // 如果是匿名,则跳过
  5512. if (uid <= 0) {
  5513. return;
  5514. }
  5515.  
  5516. // 获取隐藏模式下标
  5517. const mode = this.settings.getModeByName("隐藏");
  5518.  
  5519. // 如果当前模式不低于隐藏模式,则跳过
  5520. if (result.mode >= mode) {
  5521. return;
  5522. }
  5523.  
  5524. // 获取注册时间限制
  5525. const filterRegdateLimit = this.settings.filterRegdateLimit;
  5526.  
  5527. // 未启用则跳过
  5528. if (filterRegdateLimit <= 0) {
  5529. return;
  5530. }
  5531.  
  5532. // 没有用户信息,优先从页面上获取
  5533. if (item.userInfo === undefined) {
  5534. this.getUserInfo(item);
  5535. }
  5536.  
  5537. // 没有再从接口获取
  5538. if (item.userInfo === undefined) {
  5539. await this.getPostInfo(item);
  5540. }
  5541.  
  5542. // 获取注册时间
  5543. const { regdate } = item.userInfo || {};
  5544.  
  5545. // 获取失败则跳过
  5546. if (regdate === undefined) {
  5547. return;
  5548. }
  5549.  
  5550. // 转换时间格式,泥潭接口只精确到秒
  5551. const date = new Date(regdate * 1000);
  5552.  
  5553. // 计算时间差
  5554. const diff = Date.now() - date;
  5555.  
  5556. // 判断是否符合条件
  5557. if (diff > filterRegdateLimit) {
  5558. return;
  5559. }
  5560.  
  5561. // 转换为天数
  5562. const days = Math.floor(diff / 86400000);
  5563.  
  5564. // 更新过滤模式和原因
  5565. result.mode = mode;
  5566. result.reason = `注册时间: ${days}天`;
  5567. }
  5568.  
  5569. /**
  5570. * 根据发帖数量过滤
  5571. * @param {*} item 绑定的 nFilter
  5572. * @param {*} result 过滤结果
  5573. */
  5574. async filterByPostnum(item, result) {
  5575. const { uid } = item;
  5576.  
  5577. // 如果是匿名,则跳过
  5578. if (uid <= 0) {
  5579. return;
  5580. }
  5581.  
  5582. // 获取隐藏模式下标
  5583. const mode = this.settings.getModeByName("隐藏");
  5584.  
  5585. // 如果当前模式不低于隐藏模式,则跳过
  5586. if (result.mode >= mode) {
  5587. return;
  5588. }
  5589.  
  5590. // 获取发帖数量限制
  5591. const filterPostnumLimit = this.settings.filterPostnumLimit;
  5592.  
  5593. // 未启用则跳过
  5594. if (filterPostnumLimit <= 0) {
  5595. return;
  5596. }
  5597.  
  5598. // 没有用户信息,优先从页面上获取
  5599. if (item.userInfo === undefined) {
  5600. this.getUserInfo(item);
  5601. }
  5602.  
  5603. // 没有再从接口获取
  5604. if (item.userInfo === undefined) {
  5605. await this.getPostInfo(item);
  5606. }
  5607.  
  5608. // 获取发帖数量
  5609. const { postnum } = item.userInfo || {};
  5610.  
  5611. // 获取失败则跳过
  5612. if (postnum === undefined) {
  5613. return;
  5614. }
  5615.  
  5616. // 判断是否符合条件
  5617. if (postnum >= filterPostnumLimit) {
  5618. return;
  5619. }
  5620.  
  5621. // 更新过滤模式和原因
  5622. result.mode = mode;
  5623. result.reason = `发帖数量: ${postnum}`;
  5624. }
  5625.  
  5626. /**
  5627. * 根据发帖比例过滤
  5628. * @param {*} item 绑定的 nFilter
  5629. * @param {*} result 过滤结果
  5630. */
  5631. async filterByTopicRate(item, result) {
  5632. const { uid } = item;
  5633.  
  5634. // 如果是匿名,则跳过
  5635. if (uid <= 0) {
  5636. return;
  5637. }
  5638.  
  5639. // 获取隐藏模式下标
  5640. const mode = this.settings.getModeByName("隐藏");
  5641.  
  5642. // 如果当前模式不低于隐藏模式,则跳过
  5643. if (result.mode >= mode) {
  5644. return;
  5645. }
  5646.  
  5647. // 获取发帖比例限制
  5648. const filterTopicRateLimit = this.settings.filterTopicRateLimit;
  5649.  
  5650. // 未启用则跳过
  5651. if (filterTopicRateLimit <= 0 || filterTopicRateLimit >= 100) {
  5652. return;
  5653. }
  5654.  
  5655. // 没有用户信息,优先从页面上获取
  5656. if (item.userInfo === undefined) {
  5657. this.getUserInfo(item);
  5658. }
  5659.  
  5660. // 没有再从接口获取
  5661. if (item.userInfo === undefined) {
  5662. await this.getPostInfo(item);
  5663. }
  5664.  
  5665. // 获取发帖数量
  5666. const { postnum } = item.userInfo || {};
  5667.  
  5668. // 获取失败则跳过
  5669. if (postnum === undefined) {
  5670. return;
  5671. }
  5672.  
  5673. // 获取主题数量
  5674. const topicNum = await this.getTopicNum(item);
  5675.  
  5676. // 计算发帖比例
  5677. const topicRate = Math.ceil((topicNum / postnum) * 100);
  5678.  
  5679. // 判断是否符合条件
  5680. if (topicRate < filterTopicRateLimit) {
  5681. return;
  5682. }
  5683.  
  5684. // 更新过滤模式和原因
  5685. result.mode = mode;
  5686. result.reason = `发帖比例: ${topicRate}% (${topicNum}/${postnum})`;
  5687. }
  5688.  
  5689. /**
  5690. * 根据每页主题数量过滤
  5691. * @param {*} item 绑定的 nFilter
  5692. * @param {*} result 过滤结果
  5693. */
  5694. async filterByTopicPerPage(item, result) {
  5695. const { uid, tid } = item;
  5696.  
  5697. // 如果是匿名,则跳过
  5698. if (uid <= 0) {
  5699. return;
  5700. }
  5701.  
  5702. // 获取隐藏模式下标
  5703. const mode = this.settings.getModeByName("隐藏");
  5704.  
  5705. // 如果当前模式不低于隐藏模式,则跳过
  5706. if (result.mode >= mode) {
  5707. return;
  5708. }
  5709.  
  5710. // 获取每页主题数量限制
  5711. const filterTopicPerPageLimit = this.settings.filterTopicPerPageLimit;
  5712.  
  5713. // 未启用则跳过
  5714. if (Number.isNaN(filterTopicPerPageLimit)) {
  5715. return;
  5716. }
  5717.  
  5718. // 已有标记,直接斩杀
  5719. // 但需要判断斩杀线是否有变动
  5720. if (Object.hasOwn(this.cache.topicPerPages, uid)) {
  5721. if (this.cache.topicPerPages[uid] > filterTopicPerPageLimit) {
  5722. // 重新计算数量
  5723. const num = this.data.filter((item) => item.uid === uid).length;
  5724.  
  5725. // 更新过滤模式和原因
  5726. result.mode = mode;
  5727. result.reason = `本页主题: ${num}`;
  5728.  
  5729. return;
  5730. }
  5731.  
  5732. delete this.cache.topicPerPages[uid];
  5733. }
  5734.  
  5735. // 获取页面上所有主题
  5736. const list = [...topicModule.data].map((item) => item.nFilter || {});
  5737.  
  5738. // 预检测
  5739. const max = list.filter((item) => item.uid === uid).length;
  5740.  
  5741. // 多页的主题数量累计仍不符合,直接跳过
  5742. if (max <= filterTopicPerPageLimit) {
  5743. return;
  5744. }
  5745.  
  5746. // 获取当前主题下标
  5747. const index = list.findIndex((item) => item.tid === tid);
  5748.  
  5749. // 没有找到,跳过
  5750. if (index < 0) {
  5751. return;
  5752. }
  5753.  
  5754. // 每页主题数量
  5755. const topicPerPage = 30;
  5756.  
  5757. // 符合条件的主题下标
  5758. let indexPrev = index;
  5759. let indexNext = index;
  5760.  
  5761. // 符合的主题
  5762. let checked = 1;
  5763.  
  5764. // 从当前主题往前和往后检测
  5765. for (let i = 1; i < topicPerPage - (indexNext - indexPrev); i += 1) {
  5766. const topicPrev = list[indexPrev - i];
  5767. const topicNext = list[indexNext + i];
  5768.  
  5769. if (topicPrev === undefined && topicNext === undefined) {
  5770. break;
  5771. }
  5772.  
  5773. if (topicPrev && topicPrev.uid === uid) {
  5774. indexPrev -= i;
  5775. checked += 1;
  5776.  
  5777. i = 0;
  5778. continue;
  5779. }
  5780.  
  5781. if (topicNext && topicNext.uid === uid) {
  5782. indexNext += i;
  5783. checked += 1;
  5784.  
  5785. i = 0;
  5786. continue;
  5787. }
  5788. }
  5789.  
  5790. // 判断是否符合条件
  5791. if (checked <= filterTopicPerPageLimit) {
  5792. return;
  5793. }
  5794.  
  5795. // 写入斩杀标记
  5796. this.cache.topicPerPages[uid] = checked;
  5797.  
  5798. // 重新计算相关帖子
  5799. this.reFilter(uid);
  5800. }
  5801.  
  5802. /**
  5803. * 根据版面声望过滤
  5804. * @param {*} item 绑定的 nFilter
  5805. * @param {*} result 过滤结果
  5806. */
  5807. async filterByReputation(item, result) {
  5808. const { uid } = item;
  5809.  
  5810. // 如果是匿名,则跳过
  5811. if (uid <= 0) {
  5812. return;
  5813. }
  5814.  
  5815. // 获取隐藏模式下标
  5816. const mode = this.settings.getModeByName("隐藏");
  5817.  
  5818. // 如果当前模式不低于隐藏模式,则跳过
  5819. if (result.mode >= mode) {
  5820. return;
  5821. }
  5822.  
  5823. // 获取版面声望限制
  5824. const filterReputationLimit = this.settings.filterReputationLimit;
  5825.  
  5826. // 未启用则跳过
  5827. if (Number.isNaN(filterReputationLimit)) {
  5828. return;
  5829. }
  5830.  
  5831. // 没有声望信息,优先从页面上获取
  5832. if (item.reputation === undefined) {
  5833. this.getUserInfo(item);
  5834. }
  5835.  
  5836. // 没有再从接口获取
  5837. if (item.reputation === undefined) {
  5838. await this.getPostInfo(item);
  5839. }
  5840.  
  5841. // 获取版面声望
  5842. const reputation = item.reputation || 0;
  5843.  
  5844. // 判断是否符合条件
  5845. if (reputation >= filterReputationLimit) {
  5846. return;
  5847. }
  5848.  
  5849. // 更新过滤模式和原因
  5850. result.mode = mode;
  5851. result.reason = `版面声望: ${reputation}`;
  5852. }
  5853.  
  5854. /**
  5855. * 重新过滤
  5856. * @param {Number} uid 用户 ID
  5857. */
  5858. reFilter(uid = null) {
  5859. this.data.forEach((item) => {
  5860. if (item.uid === uid || uid === null) {
  5861. item.execute();
  5862. }
  5863. });
  5864. }
  5865. }
  5866.  
  5867. /**
  5868. * 设置模块
  5869. */
  5870. class SettingsModule extends Module {
  5871. /**
  5872. * 模块名称
  5873. */
  5874. static name = "settings";
  5875.  
  5876. /**
  5877. * 顺序
  5878. */
  5879. static order = 0;
  5880.  
  5881. /**
  5882. * 创建实例
  5883. * @param {Settings} settings 设置
  5884. * @param {API} api API
  5885. * @param {UI} ui UI
  5886. * @param {Array} data 过滤列表
  5887. * @returns {Module | null} 成功后返回模块实例
  5888. */
  5889. static create(settings, api, ui, data) {
  5890. // 读取设置里的模块列表
  5891. const modules = settings.modules;
  5892.  
  5893. // 如果不包含自己,加入列表中,因为设置模块是必须的
  5894. if (modules.includes(this.name) === false) {
  5895. settings.modules = [...modules, this.name];
  5896. }
  5897.  
  5898. // 创建实例
  5899. return super.create(settings, api, ui, data);
  5900. }
  5901.  
  5902. /**
  5903. * 初始化,增加设置
  5904. */
  5905. initComponents() {
  5906. super.initComponents();
  5907.  
  5908. const { settings, ui } = this;
  5909. const { add } = ui.views.settings;
  5910.  
  5911. // 前置过滤
  5912. {
  5913. const input = ui.createElement("INPUT", [], {
  5914. type: "checkbox",
  5915. });
  5916.  
  5917. const label = ui.createElement("LABEL", ["前置过滤", input], {
  5918. style: "display: flex;",
  5919. });
  5920.  
  5921. settings.preFilterEnabled.then((checked) => {
  5922. input.checked = checked;
  5923. input.onchange = () => {
  5924. settings.preFilterEnabled = !checked;
  5925. };
  5926. });
  5927.  
  5928. add(this.constructor.order + 0, label);
  5929. }
  5930.  
  5931. // 提醒过滤
  5932. {
  5933. const input = ui.createElement("INPUT", [], {
  5934. type: "checkbox",
  5935. });
  5936.  
  5937. const label = ui.createElement("LABEL", ["提醒过滤", input], {
  5938. style: "display: flex;",
  5939. });
  5940.  
  5941. const tips = ui.createElement("DIV", TIPS.filterNotification, {
  5942. className: "silver",
  5943. });
  5944.  
  5945. settings.notificationFilterEnabled.then((checked) => {
  5946. input.checked = checked;
  5947. input.onchange = () => {
  5948. settings.notificationFilterEnabled = !checked;
  5949. };
  5950. });
  5951.  
  5952. add(this.constructor.order + 1, label, tips);
  5953. }
  5954.  
  5955. // 模块选择
  5956. {
  5957. const modules = [
  5958. ListModule,
  5959. UserModule,
  5960. TagModule,
  5961. KeywordModule,
  5962. LocationModule,
  5963. ForumOrSubsetModule,
  5964. HunterModule,
  5965. MiscModule,
  5966. ];
  5967.  
  5968. const items = modules.map((item) => {
  5969. const input = ui.createElement("INPUT", [], {
  5970. type: "checkbox",
  5971. value: item.name,
  5972. checked: settings.modules.includes(item.name),
  5973. onchange: () => {
  5974. const checked = input.checked;
  5975.  
  5976. modules.map((m, index) => {
  5977. const isDepend = checked
  5978. ? item.depends.find((i) => i.name === m.name)
  5979. : m.depends.find((i) => i.name === item.name);
  5980.  
  5981. if (isDepend) {
  5982. const element = items[index].querySelector("INPUT");
  5983.  
  5984. if (element) {
  5985. element.checked = checked;
  5986. }
  5987. }
  5988. });
  5989. },
  5990. });
  5991.  
  5992. const label = ui.createElement("LABEL", [item.label, input], {
  5993. style: "display: flex; margin-right: 10px;",
  5994. });
  5995.  
  5996. return label;
  5997. });
  5998.  
  5999. const button = ui.createButton("确认", () => {
  6000. const checked = group.querySelectorAll("INPUT:checked");
  6001. const values = [...checked].map((item) => item.value);
  6002.  
  6003. settings.modules = values;
  6004.  
  6005. location.reload();
  6006. });
  6007.  
  6008. const group = ui.createElement("DIV", [...items, button], {
  6009. style: "display: flex;",
  6010. });
  6011.  
  6012. const label = ui.createElement("LABEL", "启用模块");
  6013.  
  6014. add(this.constructor.order + 2, label, group);
  6015. }
  6016.  
  6017. // 默认过滤模式
  6018. {
  6019. const modes = ["标记", "遮罩", "隐藏"].map((item) => {
  6020. const input = ui.createElement("INPUT", [], {
  6021. type: "radio",
  6022. name: "defaultFilterMode",
  6023. value: item,
  6024. checked: settings.defaultFilterMode === item,
  6025. onchange: () => {
  6026. settings.defaultFilterMode = item;
  6027.  
  6028. this.reFilter();
  6029. },
  6030. });
  6031.  
  6032. const label = ui.createElement("LABEL", [item, input], {
  6033. style: "display: flex; margin-right: 10px;",
  6034. });
  6035.  
  6036. return label;
  6037. });
  6038.  
  6039. const group = ui.createElement("DIV", modes, {
  6040. style: "display: flex;",
  6041. });
  6042.  
  6043. const label = ui.createElement("LABEL", "默认过滤模式");
  6044.  
  6045. const tips = ui.createElement("DIV", TIPS.filterMode, {
  6046. className: "silver",
  6047. });
  6048.  
  6049. add(this.constructor.order + 3, label, group, tips);
  6050. }
  6051.  
  6052. // 主题
  6053. {
  6054. const themes = Object.keys(THEMES).map((item) => {
  6055. const input = ui.createElement("INPUT", [], {
  6056. type: "radio",
  6057. name: "theme",
  6058. value: item,
  6059. checked: settings.theme === item,
  6060. onchange: () => {
  6061. settings.theme = item;
  6062.  
  6063. handleStyle(settings);
  6064. },
  6065. });
  6066.  
  6067. const { name } = THEMES[item];
  6068.  
  6069. const label = ui.createElement("LABEL", [name, input], {
  6070. style: "display: flex; margin-right: 10px;",
  6071. });
  6072.  
  6073. return label;
  6074. });
  6075.  
  6076. const group = ui.createElement("DIV", themes, {
  6077. style: "display: flex;",
  6078. });
  6079.  
  6080. const label = ui.createElement("LABEL", "主题");
  6081.  
  6082. add(this.constructor.order + 4, label, group);
  6083. }
  6084. }
  6085.  
  6086. /**
  6087. * 重新过滤
  6088. */
  6089. reFilter() {
  6090. // 目前仅在修改默认过滤模式时重新过滤
  6091. this.data.forEach((item) => {
  6092. // 如果过滤模式是继承,则重新过滤
  6093. if (item.filterMode === "继承") {
  6094. item.execute();
  6095. }
  6096.  
  6097. // 如果有引用,也重新过滤
  6098. if (Object.values(item.quotes || {}).includes("继承")) {
  6099. item.execute();
  6100. return;
  6101. }
  6102. });
  6103. }
  6104. }
  6105.  
  6106. /**
  6107. * 增强的列表模块,增加了用户作为附加模块
  6108. */
  6109. class ListEnhancedModule extends ListModule {
  6110. /**
  6111. * 模块名称
  6112. */
  6113. static name = "list";
  6114.  
  6115. /**
  6116. * 附加模块
  6117. */
  6118. static addons = [UserModule];
  6119.  
  6120. /**
  6121. * 附加的用户模块
  6122. * @returns {UserModule} 用户模块
  6123. */
  6124. get userModule() {
  6125. return this.addons[UserModule.name];
  6126. }
  6127.  
  6128. /**
  6129. * 表格列
  6130. * @returns {Array} 表格列集合
  6131. */
  6132. columns() {
  6133. const hasAddon = this.hasAddon(UserModule);
  6134.  
  6135. if (hasAddon === false) {
  6136. return super.columns();
  6137. }
  6138.  
  6139. return [
  6140. { label: "用户", width: 1 },
  6141. { label: "内容", ellipsis: true },
  6142. { label: "过滤模式", center: true, width: 1 },
  6143. { label: "原因", width: 1 },
  6144. { label: "操作", width: 1 },
  6145. ];
  6146. }
  6147.  
  6148. /**
  6149. * 表格项
  6150. * @param {*} item 绑定的 nFilter
  6151. * @returns {Array} 表格项集合
  6152. */
  6153. column(item) {
  6154. const column = super.column(item);
  6155.  
  6156. const hasAddon = this.hasAddon(UserModule);
  6157.  
  6158. if (hasAddon === false) {
  6159. return column;
  6160. }
  6161.  
  6162. const { ui } = this;
  6163. const { table } = this.views;
  6164. const { uid, username } = item;
  6165.  
  6166. const user = this.userModule.format(uid, username);
  6167.  
  6168. const buttons = (() => {
  6169. if (uid <= 0) {
  6170. return null;
  6171. }
  6172.  
  6173. const block = ui.createButton("屏蔽", (e) => {
  6174. this.userModule.renderDetails(uid, username, (type) => {
  6175. // 删除失效数据,等待重新过滤
  6176. table.remove(e);
  6177.  
  6178. // 如果是新增,不会因为用户重新过滤,需要主动触发
  6179. if (type === "ADD") {
  6180. this.userModule.reFilter(uid);
  6181. }
  6182. });
  6183. });
  6184.  
  6185. return ui.createButtonGroup(block);
  6186. })();
  6187.  
  6188. return [user, ...column, buttons];
  6189. }
  6190. }
  6191.  
  6192. /**
  6193. * 增强的用户模块,增加了标记作为附加模块
  6194. */
  6195. class UserEnhancedModule extends UserModule {
  6196. /**
  6197. * 模块名称
  6198. */
  6199. static name = "user";
  6200.  
  6201. /**
  6202. * 附加模块
  6203. */
  6204. static addons = [TagModule];
  6205.  
  6206. /**
  6207. * 附加的标记模块
  6208. * @returns {TagModule} 标记模块
  6209. */
  6210. get tagModule() {
  6211. return this.addons[TagModule.name];
  6212. }
  6213.  
  6214. /**
  6215. * 表格列
  6216. * @returns {Array} 表格列集合
  6217. */
  6218. columns() {
  6219. const hasAddon = this.hasAddon(TagModule);
  6220.  
  6221. if (hasAddon === false) {
  6222. return super.columns();
  6223. }
  6224.  
  6225. return [
  6226. { label: "昵称", width: 1 },
  6227. { label: "标记" },
  6228. { label: "过滤模式", center: true, width: 1 },
  6229. { label: "操作", width: 1 },
  6230. ];
  6231. }
  6232.  
  6233. /**
  6234. * 表格项
  6235. * @param {*} item 用户信息
  6236. * @returns {Array} 表格项集合
  6237. */
  6238. column(item) {
  6239. const column = super.column(item);
  6240.  
  6241. const hasAddon = this.hasAddon(TagModule);
  6242.  
  6243. if (hasAddon === false) {
  6244. return column;
  6245. }
  6246.  
  6247. const { ui } = this;
  6248. const { table } = this.views;
  6249. const { id, name } = item;
  6250.  
  6251. const tags = ui.createElement(
  6252. "DIV",
  6253. item.tags.map((id) => this.tagModule.format(id))
  6254. );
  6255.  
  6256. const newColumn = [...column];
  6257.  
  6258. newColumn.splice(1, 0, tags);
  6259.  
  6260. const buttons = column[column.length - 1];
  6261.  
  6262. const update = ui.createButton("编辑", (e) => {
  6263. this.renderDetails(id, name, (type, newValue) => {
  6264. if (type === "UPDATE") {
  6265. table.update(e, ...this.column(newValue));
  6266. }
  6267.  
  6268. if (type === "REMOVE") {
  6269. table.remove(e);
  6270. }
  6271. });
  6272. });
  6273.  
  6274. buttons.insertBefore(update, buttons.firstChild);
  6275.  
  6276. return newColumn;
  6277. }
  6278.  
  6279. /**
  6280. * 渲染详情
  6281. * @param {Number} uid 用户 ID
  6282. * @param {String | undefined} name 用户名称
  6283. * @param {Function} callback 回调函数
  6284. */
  6285. renderDetails(uid, name, callback = () => {}) {
  6286. const hasAddon = this.hasAddon(TagModule);
  6287.  
  6288. if (hasAddon === false) {
  6289. return super.renderDetails(uid, name, callback);
  6290. }
  6291.  
  6292. const { ui, settings } = this;
  6293.  
  6294. // 只允许同时存在一个详情页
  6295. if (this.views.details) {
  6296. if (this.views.details.parentNode) {
  6297. this.views.details.parentNode.removeChild(this.views.details);
  6298. }
  6299. }
  6300.  
  6301. // 获取用户信息
  6302. const user = this.get(uid);
  6303.  
  6304. if (user) {
  6305. name = user.name;
  6306. }
  6307.  
  6308. // TODO 需要优化
  6309.  
  6310. const title =
  6311. (user ? "编辑" : "添加") + `用户 - ${name ? name : "#" + uid}`;
  6312.  
  6313. const table = ui.createTable([]);
  6314.  
  6315. {
  6316. const size = Math.floor((screen.width * 0.8) / 200);
  6317.  
  6318. const items = Object.values(this.tagModule.list).map(({ id }) => {
  6319. const checked = user && user.tags.includes(id) ? "checked" : "";
  6320.  
  6321. return `
  6322. <td class="c1">
  6323. <label for="s-tag-${id}" style="display: block; cursor: pointer;">
  6324. ${this.tagModule.format(id).outerHTML}
  6325. </label>
  6326. </td>
  6327. <td class="c2" width="1">
  6328. <input id="s-tag-${id}" type="checkbox" value="${id}" ${checked}/>
  6329. </td>
  6330. `;
  6331. });
  6332.  
  6333. const rows = [...new Array(Math.ceil(items.length / size))].map(
  6334. (_, index) => `
  6335. <tr class="row${(index % 2) + 1}">
  6336. ${items.slice(size * index, size * (index + 1)).join("")}
  6337. </tr>
  6338. `
  6339. );
  6340.  
  6341. table.querySelector("TBODY").innerHTML = rows.join("");
  6342. }
  6343.  
  6344. const input = ui.createElement("INPUT", [], {
  6345. type: "text",
  6346. placeholder: TIPS.addTags,
  6347. style: "width: -webkit-fill-available;",
  6348. });
  6349.  
  6350. const inputWrapper = ui.createElement("DIV", input, {
  6351. style: "margin-top: 10px;",
  6352. });
  6353.  
  6354. const filterMode = user ? user.filterMode : settings.filterModes[0];
  6355.  
  6356. const switchMode = ui.createButton(filterMode, () => {
  6357. const newMode = settings.switchModeByName(switchMode.innerText);
  6358.  
  6359. switchMode.innerText = newMode;
  6360. });
  6361.  
  6362. const buttons = ui.createElement(
  6363. "DIV",
  6364. (() => {
  6365. const remove = user
  6366. ? ui.createButton("删除", () => {
  6367. ui.confirm().then(() => {
  6368. this.remove(uid);
  6369.  
  6370. this.views.details._.hide();
  6371.  
  6372. callback("REMOVE");
  6373. });
  6374. })
  6375. : null;
  6376.  
  6377. const save = ui.createButton("保存", () => {
  6378. const checked = [...table.querySelectorAll("INPUT:checked")].map(
  6379. (input) => parseInt(input.value, 10)
  6380. );
  6381.  
  6382. const newTags = input.value
  6383. .split("|")
  6384. .filter((item) => item.length)
  6385. .map((item) => this.tagModule.add(item))
  6386. .filter((tag) => tag !== null)
  6387. .map((tag) => tag.id);
  6388.  
  6389. const tags = [...new Set([...checked, ...newTags])].sort();
  6390.  
  6391. if (user === null) {
  6392. const entity = this.add(uid, {
  6393. id: uid,
  6394. name,
  6395. tags,
  6396. filterMode: switchMode.innerText,
  6397. });
  6398.  
  6399. this.views.details._.hide();
  6400.  
  6401. callback("ADD", entity);
  6402. } else {
  6403. const entity = this.update(uid, {
  6404. name,
  6405. tags,
  6406. filterMode: switchMode.innerText,
  6407. });
  6408.  
  6409. this.views.details._.hide();
  6410.  
  6411. callback("UPDATE", entity);
  6412. }
  6413. });
  6414.  
  6415. return ui.createButtonGroup(remove, save);
  6416. })(),
  6417. {
  6418. className: "right_",
  6419. }
  6420. );
  6421.  
  6422. const actions = ui.createElement(
  6423. "DIV",
  6424. [ui.createElement("SPAN", "过滤模式:"), switchMode, buttons],
  6425. {
  6426. style: "margin-top: 10px;",
  6427. }
  6428. );
  6429.  
  6430. const tips = ui.createElement("DIV", TIPS.filterMode, {
  6431. className: "silver",
  6432. style: "margin-top: 10px;",
  6433. });
  6434.  
  6435. const content = ui.createElement(
  6436. "DIV",
  6437. [table, inputWrapper, actions, tips],
  6438. {
  6439. style: "width: 80vw",
  6440. }
  6441. );
  6442.  
  6443. // 创建弹出框
  6444. this.views.details = ui.createDialog(null, title, content);
  6445. }
  6446. }
  6447.  
  6448. /**
  6449. * 处理 topicArg 模块
  6450. * @param {Filter} filter 过滤器
  6451. * @param {*} value commonui.topicArg
  6452. */
  6453. const handleTopicModule = async (filter, value) => {
  6454. // 绑定主题模块
  6455. topicModule = value;
  6456.  
  6457. if (value === undefined) {
  6458. return;
  6459. }
  6460.  
  6461. // 是否启用前置过滤
  6462. const preFilterEnabled = await filter.settings.preFilterEnabled;
  6463.  
  6464. // 前置过滤
  6465. // 先直接隐藏,等过滤完毕后再放出来
  6466. const beforeGet = (...args) => {
  6467. if (preFilterEnabled) {
  6468. // 主题标题
  6469. const title = document.getElementById(args[1]);
  6470.  
  6471. // 主题容器
  6472. const container = title.closest("tr");
  6473.  
  6474. // 隐藏元素
  6475. container.style.display = "none";
  6476. }
  6477.  
  6478. return args;
  6479. };
  6480.  
  6481. // 过滤
  6482. const afterGet = (_, args) => {
  6483. // 主题 ID
  6484. const tid = args[8];
  6485.  
  6486. // 回复 ID
  6487. const pid = args[9];
  6488.  
  6489. // 找到对应数据
  6490. const data = topicModule.data.find(
  6491. (item) => item[8] === tid && item[9] === pid
  6492. );
  6493.  
  6494. // 开始过滤
  6495. if (data) {
  6496. filter.filterTopic(data);
  6497. }
  6498. };
  6499.  
  6500. // 如果已经有数据,则直接过滤
  6501. Object.values(topicModule.data).forEach((item) => {
  6502. filter.filterTopic(item);
  6503. });
  6504.  
  6505. // 拦截 add 函数,这是泥潭的主题添加事件
  6506. Tools.interceptProperty(topicModule, "add", {
  6507. beforeGet,
  6508. afterGet,
  6509. });
  6510. };
  6511.  
  6512. /**
  6513. * 处理 postArg 模块
  6514. * @param {Filter} filter 过滤器
  6515. * @param {*} value commonui.postArg
  6516. */
  6517. const handleReplyModule = async (filter, value) => {
  6518. // 绑定回复模块
  6519. replyModule = value;
  6520.  
  6521. if (value === undefined) {
  6522. return;
  6523. }
  6524.  
  6525. // 是否启用前置过滤
  6526. const preFilterEnabled = await filter.settings.preFilterEnabled;
  6527.  
  6528. // 前置过滤
  6529. // 先直接隐藏,等过滤完毕后再放出来
  6530. const beforeGet = (...args) => {
  6531. if (preFilterEnabled) {
  6532. // 楼层号
  6533. const index = args[0];
  6534.  
  6535. // 判断是否是楼层
  6536. const isFloor = typeof index === "number";
  6537.  
  6538. // 评论额外标签
  6539. const prefix = isFloor ? "" : "comment";
  6540.  
  6541. // 用户容器
  6542. const uInfoC = document.querySelector(`#${prefix}posterinfo${index}`);
  6543.  
  6544. // 回复容器
  6545. const container = isFloor
  6546. ? uInfoC.closest("tr")
  6547. : uInfoC.closest(".comment_c");
  6548.  
  6549. // 隐藏元素
  6550. container.style.display = "none";
  6551. }
  6552.  
  6553. return args;
  6554. };
  6555.  
  6556. // 过滤
  6557. const afterGet = (_, args) => {
  6558. // 楼层号
  6559. const index = args[0];
  6560.  
  6561. // 找到对应数据
  6562. const data = replyModule.data[index];
  6563.  
  6564. // 开始过滤
  6565. if (data) {
  6566. filter.filterReply(data);
  6567. }
  6568. };
  6569.  
  6570. // 如果已经有数据,则直接过滤
  6571. Object.values(replyModule.data).forEach((item) => {
  6572. filter.filterReply(item);
  6573. });
  6574.  
  6575. // 拦截 proc 函数,这是泥潭的回复添加事件
  6576. Tools.interceptProperty(replyModule, "proc", {
  6577. beforeGet,
  6578. afterGet,
  6579. });
  6580. };
  6581.  
  6582. /**
  6583. * 处理 notification 模块
  6584. * @param {Filter} filter 过滤器
  6585. * @param {*} value commonui.notification
  6586. */
  6587. const handleNotificationModule = async (filter, value) => {
  6588. // 绑定提醒模块
  6589. notificationModule = value;
  6590.  
  6591. if (value === undefined) {
  6592. return;
  6593. }
  6594.  
  6595. // 是否启用提醒过滤
  6596. const notificationFilterEnabled = await filter.settings
  6597. .notificationFilterEnabled;
  6598.  
  6599. if (notificationFilterEnabled === false) {
  6600. return;
  6601. }
  6602.  
  6603. // 由于过滤需要异步进行,所以要重写泥潭的提醒弹窗展示事件,在此处理需要过滤的数据,并根据结果确认是否展示弹窗
  6604. const { openBox } = value;
  6605.  
  6606. // 重写泥潭的提醒弹窗展示事件
  6607. notificationModule.openBox = async (...args) => {
  6608. // 回复列表,小屏幕下也作为短消息和系统提醒,参考 js_notification.js 的 createBox
  6609. const replyList = notificationModule._tab[0];
  6610.  
  6611. // 筛选出有作者的条目
  6612. const authorList = replyList.querySelectorAll("A[href^='/nuke.php']");
  6613.  
  6614. // 依次过滤
  6615. await Promise.all(
  6616. [...authorList].map(async (item) => {
  6617. // 找到提醒主容器
  6618. const container = item.closest("SPAN[class^='bg']");
  6619.  
  6620. // 找到回复链接
  6621. const reply = container.querySelector("A[href^='/read.php?pid=']");
  6622.  
  6623. // 找到主题链接
  6624. const topic = container.querySelector("A[href^='/read.php?tid=']");
  6625.  
  6626. // 不符合则跳过
  6627. if (reply === null || topic === null) {
  6628. return;
  6629. }
  6630.  
  6631. // 获取 UID 和 昵称
  6632. const { uid, username } = (() => {
  6633. const res = item.getAttribute("href").match(/uid=(\S+)/);
  6634.  
  6635. if (res) {
  6636. return {
  6637. uid: parseInt(res[1], 10),
  6638. username: item.innerText,
  6639. };
  6640. }
  6641.  
  6642. return {};
  6643. })();
  6644.  
  6645. // 获取 PID
  6646. const pid = (() => {
  6647. const res = reply.getAttribute("href").match(/pid=(\S+)/);
  6648.  
  6649. if (res) {
  6650. return parseInt(res[1], 10);
  6651. }
  6652.  
  6653. return 0;
  6654. })();
  6655.  
  6656. // 获取 TID
  6657. const tid = (() => {
  6658. const res = topic.getAttribute("href").match(/tid=(\S+)/);
  6659.  
  6660. if (res) {
  6661. return parseInt(res[1], 10);
  6662. }
  6663.  
  6664. return 0;
  6665. })();
  6666.  
  6667. // 过滤
  6668. await filter.filterNotification(container, {
  6669. tid,
  6670. pid,
  6671. uid,
  6672. username,
  6673. subject: "",
  6674. });
  6675. })
  6676. );
  6677.  
  6678. // 判断过滤后是否还有可见的数据
  6679. let visible = false;
  6680.  
  6681. for (const tab in notificationModule._tab) {
  6682. const items =
  6683. notificationModule._tab[tab].querySelectorAll("SPAN[class^='bg']");
  6684.  
  6685. const filtered = [...items].filter((item) => {
  6686. return item.style.display !== "none";
  6687. });
  6688.  
  6689. if (filtered.length > 0) {
  6690. visible = true;
  6691. break;
  6692. }
  6693.  
  6694. notificationModule._tab[tab].style.display = "none";
  6695. }
  6696.  
  6697. // 显示弹窗
  6698. if (visible) {
  6699. openBox.apply(notificationModule, args);
  6700. }
  6701. };
  6702. };
  6703.  
  6704. /**
  6705. * 处理样式
  6706. * @param {Settings} settings 设置
  6707. */
  6708. const handleStyle = (settings) => {
  6709. const afterSet = (value) => {
  6710. if (value === undefined) {
  6711. return;
  6712. }
  6713.  
  6714. // 更新样式
  6715. Object.assign(THEMES["system"], {
  6716. fontColor: value.gbg3,
  6717. borderColor: value.gbg4,
  6718. backgroundColor: value.gbg4,
  6719. });
  6720.  
  6721. // 读取设置
  6722. const theme = settings.theme;
  6723.  
  6724. // 读取样式
  6725. const { fontColor, borderColor, backgroundColor } = (() => {
  6726. if (Object.hasOwn(THEMES, theme)) {
  6727. return THEMES[theme];
  6728. }
  6729.  
  6730. return THEMES["system"];
  6731. })();
  6732.  
  6733. // 更新样式
  6734. Tools.addStyle(
  6735. `
  6736. .filter-table-wrapper {
  6737. max-height: 80vh;
  6738. overflow-y: auto;
  6739. }
  6740. .filter-table {
  6741. margin: 0;
  6742. }
  6743. .filter-table th,
  6744. .filter-table td {
  6745. position: relative;
  6746. white-space: nowrap;
  6747. }
  6748. .filter-table th {
  6749. position: sticky;
  6750. top: 2px;
  6751. z-index: 1;
  6752. }
  6753. .filter-table input:not([type]), .filter-table input[type="text"] {
  6754. margin: 0;
  6755. box-sizing: border-box;
  6756. height: 100%;
  6757. width: 100%;
  6758. }
  6759. .filter-input-wrapper {
  6760. position: absolute;
  6761. top: 6px;
  6762. right: 6px;
  6763. bottom: 6px;
  6764. left: 6px;
  6765. }
  6766. .filter-text-ellipsis {
  6767. display: flex;
  6768. }
  6769. .filter-text-ellipsis > * {
  6770. flex: 1;
  6771. width: 1px;
  6772. overflow: hidden;
  6773. text-overflow: ellipsis;
  6774. }
  6775. .filter-button-group {
  6776. margin: -.1em -.2em;
  6777. }
  6778. .filter-tags {
  6779. margin: 2px -0.2em 0;
  6780. text-align: left;
  6781. }
  6782. .filter-mask {
  6783. margin: 1px;
  6784. color: ${backgroundColor};
  6785. background: ${backgroundColor};
  6786. }
  6787. .filter-mask:hover {
  6788. color: ${backgroundColor};
  6789. }
  6790. .filter-mask * {
  6791. background: ${backgroundColor};
  6792. }
  6793. .filter-mask-block {
  6794. display: block;
  6795. border: 1px solid ${borderColor};
  6796. text-align: center;
  6797. }
  6798. .filter-mask-collapse {
  6799. border: 1px solid ${borderColor};
  6800. background: ${backgroundColor};
  6801. }
  6802. .filter-mask-hint {
  6803. color: ${fontColor};
  6804. }
  6805. .filter-input-wrapper {
  6806. position: absolute;
  6807. top: 6px;
  6808. right: 6px;
  6809. bottom: 6px;
  6810. left: 6px;
  6811. }
  6812. `,
  6813. "s-filter"
  6814. );
  6815. };
  6816.  
  6817. if (unsafeWindow.__COLOR) {
  6818. afterSet(unsafeWindow.__COLOR);
  6819. return;
  6820. }
  6821.  
  6822. Tools.interceptProperty(unsafeWindow, "__COLOR", {
  6823. afterSet,
  6824. });
  6825. };
  6826.  
  6827. /**
  6828. * 处理 commonui 模块
  6829. * @param {Filter} filter 过滤器
  6830. * @param {*} value commonui
  6831. */
  6832. const handleCommonui = (filter, value) => {
  6833. // 绑定主模块
  6834. commonui = value;
  6835.  
  6836. // 拦截 mainMenu 模块,UI 需要在 init 后加载
  6837. Tools.interceptProperty(commonui, "mainMenu", {
  6838. afterSet: (value) => {
  6839. Tools.interceptProperty(value, "init", {
  6840. afterGet: () => {
  6841. filter.ui.render();
  6842. },
  6843. afterSet: () => {
  6844. filter.ui.render();
  6845. },
  6846. });
  6847. },
  6848. });
  6849.  
  6850. // 拦截 topicArg 模块,这是泥潭的主题入口
  6851. Tools.interceptProperty(commonui, "topicArg", {
  6852. afterSet: (value) => {
  6853. handleTopicModule(filter, value);
  6854. },
  6855. });
  6856.  
  6857. // 拦截 postArg 模块,这是泥潭的回复入口
  6858. Tools.interceptProperty(commonui, "postArg", {
  6859. afterSet: (value) => {
  6860. handleReplyModule(filter, value);
  6861. },
  6862. });
  6863.  
  6864. // 拦截 notification 模块,这是泥潭的提醒入口
  6865. Tools.interceptProperty(commonui, "notification", {
  6866. afterSet: (value) => {
  6867. handleNotificationModule(filter, value);
  6868. },
  6869. });
  6870. };
  6871.  
  6872. /**
  6873. * 注册脚本菜单
  6874. * @param {Settings} settings 设置
  6875. */
  6876. const registerMenu = async (settings) => {
  6877. const enabled = await settings.preFilterEnabled;
  6878.  
  6879. GM_registerMenuCommand(`前置过滤:${enabled ? "是" : "否"}`, () => {
  6880. settings.preFilterEnabled = !enabled;
  6881. });
  6882. };
  6883.  
  6884. // 主函数
  6885. (async () => {
  6886. // 初始化缓存和 API
  6887. const { cache, api } = initCacheAndAPI();
  6888.  
  6889. // 初始化设置
  6890. const settings = new Settings(cache);
  6891.  
  6892. // 读取设置
  6893. await settings.load();
  6894.  
  6895. // 初始化 UI
  6896. const ui = new UI(settings, api);
  6897.  
  6898. // 初始化过滤器
  6899. const filter = new Filter(settings, api, ui);
  6900.  
  6901. // 加载模块
  6902. filter.initModules(
  6903. SettingsModule,
  6904. ListEnhancedModule,
  6905. UserEnhancedModule,
  6906. TagModule,
  6907. KeywordModule,
  6908. LocationModule,
  6909. ForumOrSubsetModule,
  6910. HunterModule,
  6911. MiscModule
  6912. );
  6913.  
  6914. // 注册脚本菜单
  6915. registerMenu(settings);
  6916.  
  6917. // 处理样式
  6918. handleStyle(settings);
  6919.  
  6920. // 处理 commonui 模块
  6921. if (unsafeWindow.commonui) {
  6922. handleCommonui(filter, unsafeWindow.commonui);
  6923. return;
  6924. }
  6925.  
  6926. Tools.interceptProperty(unsafeWindow, "commonui", {
  6927. afterSet: (value) => {
  6928. handleCommonui(filter, value);
  6929. },
  6930. });
  6931. })();
  6932. })();