NGA Filter

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

当前为 2025-03-13 提交的版本,查看 最新版本

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