NGA Filter

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

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

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