NGA Filter

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

当前为 2024-07-25 提交的版本,查看 最新版本

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