NGA Filter

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

当前为 2024-03-30 提交的版本,查看 最新版本

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