NGA Filter

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

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

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