NGA UserInfo Enhance

隐藏不需要的用户信息,或显示额外的用户信息,包括被点赞和粉丝数量,以及坛龄、离线天数、发帖数量、属地、曾用名、游戏档案、刀塔段位

// ==UserScript==
// @name              NGA UserInfo Enhance
// @name:zh-CN        NGA 用户信息增强

// @namespace         https://greasyfork.org/users/263018
// @version           2.0.7
// @author            snyssss
// @license           MIT

// @description       隐藏不需要的用户信息,或显示额外的用户信息,包括被点赞和粉丝数量,以及坛龄、离线天数、发帖数量、属地、曾用名、游戏档案、刀塔段位
// @description:zh-CN 隐藏不需要的用户信息,或显示额外的用户信息,包括被点赞和粉丝数量,以及坛龄、离线天数、发帖数量、属地、曾用名、游戏档案、刀塔段位

// @match             *://bbs.nga.cn/*
// @match             *://ngabbs.com/*
// @match             *://nga.178.com/*

// @require           https://update.greasyfork.org/scripts/486070/1405682/NGA%20Library.js

// @grant             GM_addStyle
// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_registerMenuCommand
// @grant             unsafeWindow

// @run-at            document-start
// @noframes
// ==/UserScript==

(() => {
  // 声明泥潭主模块
  let commonui;

  // 声明缓存和 API
  let cache, api;

  // 系统标签
  const SYSTEM_LABEL_MAP = {
    头像: "",
    头衔: "",
    声望: "",
    威望: "",
    级别: "",
    注册: "",
    发帖: "泥潭默认仅版主可见,普通用户可在增强里打开",
    财富: "",
    徽章: "",
    版面: "",
    备注: "",
    签名: "",
  };

  // 自定义标签
  const CUSTOM_LABEL_MAP = {
    点赞: "需要占用额外的资源",
    粉丝: "需要占用额外的资源",
    坛龄: "",
    离线: "",
    发帖: "",
    属地: "需要占用额外的资源",
    曾用名: "需要占用额外的资源",
    游戏档案: "需要占用额外的资源,目前支持 Steam、PSN、NS、原神、深空之眼",
    刀塔段位:
      "需要占用额外的资源,需要可以访问 Opendota 和 Stratz<br/>免费接口为每天 2000 次,每分钟 60 次",
  };

  // STYLE
  GM_addStyle(`
    .s-table-wrapper {
        max-height: 80vh;
        overflow-y: auto;
    }
    .s-table {
        margin: 0;
    }
    .s-table th,
    .s-table td {
        position: relative;
        white-space: nowrap;
    }
    .s-table th {
        position: sticky;
        top: 2px;
        z-index: 1;
    }
    .s-table input:not([type]), .s-table input[type="text"] {
        margin: 0;
        box-sizing: border-box;
        height: 100%;
        width: 100%;
    }
    .s-input-wrapper {
        position: absolute;
        top: 6px;
        right: 6px;
        bottom: 6px;
        left: 6px;
    }
    .s-text-ellipsis {
        display: flex;
    }
    .s-text-ellipsis > * {
        flex: 1;
        width: 1px;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .s-button-group {
        margin: -.1em -.2em;
    }
    .s-user-enhance [s-user-enhance-visible="true"].usercol::after {
        content: ' · ';
    }
    .s-user-enhance [s-user-enhance-visible="false"] {
        display: none;
    }
    `);

  /**
   * UI
   */
  class UI {
    /**
     * 标签
     */
    static label = "用户信息增强";

    /**
     * 弹出窗
     */
    window;

    /**
     * 视图元素
     */
    views = {};

    /**
     * 初始化
     */
    constructor() {
      this.init();
    }

    /**
     * 初始化,创建基础视图,初始化通用设置
     */
    init() {
      const tabs = this.createTabs({
        className: "right_",
      });

      const content = this.createElement("DIV", [], {
        style: "width: 600px;",
      });

      const container = this.createElement("DIV", [tabs, content]);

      this.views = {
        tabs,
        content,
        container,
      };
    }

    /**
     * 创建元素
     * @param   {String}                               tagName    标签
     * @param   {HTMLElement | HTMLElement[] | String} content    内容,元素或者 innerHTML
     * @param   {*}                                    properties 额外属性
     * @returns {HTMLElement}                                     元素
     */
    createElement(tagName, content, properties = {}) {
      const element = document.createElement(tagName);

      // 写入内容
      if (typeof content === "string") {
        element.innerHTML = content;
      } else {
        if (Array.isArray(content) === false) {
          content = [content];
        }

        content.forEach((item) => {
          if (item === null) {
            return;
          }

          if (typeof item === "string") {
            element.append(item);
            return;
          }

          element.appendChild(item);
        });
      }

      // 对 A 标签的额外处理
      if (tagName.toUpperCase() === "A") {
        if (Object.hasOwn(properties, "href") === false) {
          properties.href = "javascript: void(0);";
        }
      }

      // 附加属性
      Object.entries(properties).forEach(([key, value]) => {
        element[key] = value;
      });

      return element;
    }

    /**
     * 创建按钮
     * @param {String}   text       文字
     * @param {Function} onclick    点击事件
     * @param {*}        properties 额外属性
     */
    createButton(text, onclick, properties = {}) {
      return this.createElement("BUTTON", text, {
        ...properties,
        onclick,
      });
    }

    /**
     * 创建按钮组
     * @param {Array} buttons 按钮集合
     */
    createButtonGroup(...buttons) {
      return this.createElement("DIV", buttons, {
        className: "s-button-group",
      });
    }

    /**
     * 创建表格
     * @param   {Array}       headers    表头集合
     * @param   {*}           properties 额外属性
     * @returns {HTMLElement}            元素和相关函数
     */
    createTable(headers, properties = {}) {
      const rows = [];

      const ths = headers.map((item, index) =>
        this.createElement("TH", item.label, {
          ...item,
          className: `c${index + 1}`,
        })
      );

      const tr =
        ths.length > 0
          ? this.createElement("TR", ths, {
              className: "block_txt_c0",
            })
          : null;

      const thead = tr !== null ? this.createElement("THEAD", tr) : null;

      const tbody = this.createElement("TBODY", []);

      const table = this.createElement("TABLE", [thead, tbody], {
        ...properties,
        className: "s-table forumbox",
      });

      const wrapper = this.createElement("DIV", table, {
        className: "s-table-wrapper",
      });

      const intersectionObserver = new IntersectionObserver((entries) => {
        if (entries[0].intersectionRatio <= 0) return;

        const list = rows.splice(0, 10);

        if (list.length === 0) {
          return;
        }

        intersectionObserver.disconnect();

        tbody.append(...list);

        intersectionObserver.observe(tbody.lastElementChild);
      });

      const add = (...columns) => {
        const tds = columns.map((column, index) => {
          if (ths[index]) {
            const { center, ellipsis } = ths[index];

            const properties = {};

            if (center) {
              properties.style = "text-align: center;";
            }

            if (ellipsis) {
              properties.className = "s-text-ellipsis";
            }

            column = this.createElement("DIV", column, properties);
          }

          return this.createElement("TD", column, {
            className: `c${index + 1}`,
          });
        });

        const tr = this.createElement("TR", tds, {
          className: `row${(rows.length % 2) + 1}`,
        });

        intersectionObserver.disconnect();

        rows.push(tr);

        intersectionObserver.observe(tbody.lastElementChild || tbody);
      };

      const update = (e, ...columns) => {
        const row = e.target.closest("TR");

        if (row) {
          const tds = row.querySelectorAll("TD");

          columns.map((column, index) => {
            if (ths[index]) {
              const { center, ellipsis } = ths[index];

              const properties = {};

              if (center) {
                properties.style = "text-align: center;";
              }

              if (ellipsis) {
                properties.className = "s-text-ellipsis";
              }

              column = this.createElement("DIV", column, properties);
            }

            if (tds[index]) {
              tds[index].innerHTML = "";
              tds[index].append(column);
            }
          });
        }
      };

      const remove = (e) => {
        const row = e.target.closest("TR");

        if (row) {
          tbody.removeChild(row);
        }
      };

      const clear = () => {
        rows.splice(0);
        intersectionObserver.disconnect();

        tbody.innerHTML = "";
      };

      Object.assign(wrapper, {
        add,
        update,
        remove,
        clear,
      });

      return wrapper;
    }

    /**
     * 创建标签组
     * @param {*} properties 额外属性
     */
    createTabs(properties = {}) {
      const tabs = this.createElement(
        "DIV",
        `<table class="stdbtn" cellspacing="0">
                <tbody>
                  <tr></tr>
                </tbody>
              </table>`,
        properties
      );

      return this.createElement(
        "DIV",
        [
          tabs,
          this.createElement("DIV", [], {
            className: "clear",
          }),
        ],
        {
          style: "display: none; margin-bottom: 5px;",
        }
      );
    }

    /**
     * 创建标签
     * @param {Element} tabs       标签组
     * @param {String}  label      标签名称
     * @param {Number}  order      标签顺序,重复则跳过
     * @param {*}       properties 额外属性
     */
    createTab(tabs, label, order, properties = {}) {
      const group = tabs.querySelector("TR");

      const items = [...group.childNodes];

      if (items.find((item) => item.order === order)) {
        return;
      }

      if (items.length > 0) {
        tabs.style.removeProperty("display");
      }

      const tab = this.createElement("A", label, {
        ...properties,
        className: "nobr silver",
        onclick: () => {
          if (tab.className === "nobr") {
            return;
          }

          group.querySelectorAll("A").forEach((item) => {
            if (item === tab) {
              item.className = "nobr";
            } else {
              item.className = "nobr silver";
            }
          });

          if (properties.onclick) {
            properties.onclick();
          }
        },
      });

      const wrapper = this.createElement("TD", tab, {
        order,
      });

      const anchor = items.find((item) => item.order > order);

      group.insertBefore(wrapper, anchor || null);

      return wrapper;
    }

    /**
     * 创建对话框
     * @param {HTMLElement | null} anchor  要绑定的元素,如果为空,直接弹出
     * @param {String}             title   对话框的标题
     * @param {HTMLElement}        content 对话框的内容
     */
    createDialog(anchor, title, content) {
      let window;

      const show = () => {
        if (window === undefined) {
          window = commonui.createCommmonWindow();
        }

        window._.addContent(null);
        window._.addTitle(title);
        window._.addContent(content);
        window._.show();
      };

      if (anchor) {
        anchor.onclick = show;
      } else {
        show();
      }

      return window;
    }

    /**
     * 渲染视图
     */
    renderView() {
      // 创建或打开弹出窗
      if (this.window === undefined) {
        this.window = this.createDialog(
          this.views.anchor,
          this.constructor.label,
          this.views.container
        );
      } else {
        this.window._.show();
      }

      // 启用第一个模块
      this.views.tabs.querySelector("A").click();
    }

    /**
     * 渲染
     */
    render() {
      this.renderView();
    }
  }

  /**
   * 基础模块
   */
  class Module {
    /**
     * 模块名称
     */
    static name;

    /**
     * 模块标签
     */
    static label;

    /**
     * 顺序
     */
    static order;

    /**
     * UI
     */
    ui;

    /**
     * 视图元素
     */
    views = {};

    /**
     * 初始化并绑定UI,注册 UI
     * @param {UI} ui UI
     */
    constructor(ui) {
      this.ui = ui;

      this.init();
    }

    /**
     * 获取列表
     */
    get list() {
      return GM_getValue(this.constructor.name, []);
    }

    /**
     * 写入列表
     */
    set list(value) {
      GM_setValue(this.constructor.name, value);
    }

    /**
     * 切换启用状态
     * @param {String} label 标签
     */
    toggle(label) {
      const list = this.list;

      if (this.list.includes(label)) {
        this.list = list.filter((i) => i !== label);
      } else {
        this.list = list.concat(label);
      }

      rerender();
    }

    /**
     * 初始化,创建基础视图和组件
     */
    init() {
      if (this.views.container) {
        this.destroy();
      }

      const { ui } = this;

      const container = ui.createElement("DIV", []);

      this.views = {
        container,
      };

      this.initComponents();
    }

    /**
     * 初始化组件
     */
    initComponents() {}

    /**
     * 销毁
     */
    destroy() {
      Object.values(this.views).forEach((view) => {
        if (view.parentNode) {
          view.parentNode.removeChild(view);
        }
      });

      this.views = {};
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      container.innerHTML = "";
      container.appendChild(this.views.container);
    }
  }

  /**
   * 系统模块
   */
  class SystemModule extends Module {
    /**
     * 模块名称
     */
    static name = "system";

    /**
     * 模块标签
     */
    static label = "系统";

    /**
     * 顺序
     */
    static order = 10;

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "标题" },
        { label: "注释" },
        { label: "是否启用", center: true, width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {String} label       标签
     * @param   {String} description 注释
     * @returns {Array}              表格项集合
     */
    column(label, description) {
      const { ui, list } = this;

      // 标题
      const labelElement = ui.createElement("SPAN", label, {
        className: "nobr",
      });

      // 注释
      const descriptionElement = ui.createElement("SPAN", description, {
        className: "nobr",
      });

      // 是否启用
      const enabled = ui.createElement("INPUT", [], {
        type: "checkbox",
        checked: list.includes(label) === false,
        onchange: () => {
          this.toggle(label);
        },
      });

      return [labelElement, descriptionElement, enabled];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { tabs, content } = this.ui.views;

      const table = this.ui.createTable(this.columns());

      const tab = this.ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.entries(SYSTEM_LABEL_MAP).forEach(([label, description]) => {
          const column = this.column(label, description);

          add(...column);
        });
      }
    }
  }

  /**
   * 自定义模块
   */
  class CustomModule extends Module {
    /**
     * 模块名称
     */
    static name = "custom";

    /**
     * 模块标签
     */
    static label = "增强";

    /**
     * 顺序
     */
    static order = 20;

    /**
     * 表格列
     * @returns {Array} 表格列集合
     */
    columns() {
      return [
        { label: "标题" },
        { label: "注释" },
        { label: "是否启用", center: true, width: 1 },
      ];
    }

    /**
     * 表格项
     * @param   {String} label       标签
     * @param   {String} description 注释
     * @returns {Array}              表格项集合
     */
    column(label, description) {
      const { ui, list } = this;

      // 标题
      const labelElement = ui.createElement("SPAN", label, {
        className: "nobr",
      });

      // 注释
      const descriptionElement = ui.createElement("SPAN", description, {
        className: "nobr",
      });

      // 是否启用
      const enabled = ui.createElement("INPUT", [], {
        type: "checkbox",
        checked: list.includes(label),
        onchange: () => {
          this.toggle(label);
        },
      });

      return [labelElement, descriptionElement, enabled];
    }

    /**
     * 初始化组件
     */
    initComponents() {
      super.initComponents();

      const { tabs, content } = this.ui.views;

      const table = this.ui.createTable(this.columns());

      const tab = this.ui.createTab(
        tabs,
        this.constructor.label,
        this.constructor.order,
        {
          onclick: () => {
            this.render(content);
          },
        }
      );

      Object.assign(this.views, {
        tab,
        table,
      });

      this.views.container.appendChild(table);
    }

    /**
     * 渲染
     * @param {HTMLElement} container 容器
     */
    render(container) {
      super.render(container);

      const { table } = this.views;

      if (table) {
        const { add, clear } = table;

        clear();

        Object.entries(CUSTOM_LABEL_MAP).forEach(([label, description]) => {
          const column = this.column(label, description);

          add(...column);
        });
      }
    }
  }

  /**
   * 处理 commonui 模块
   * @param {*} value commonui
   */
  const handleCommonui = (value) => {
    // 绑定主模块
    commonui = value;

    // 拦截 postDisp 事件,这是泥潭的楼层渲染
    Tools.interceptProperty(commonui, "postDisp", {
      afterSet: () => {
        rerender();
      },
      afterGet: (_, args) => {
        rerender(...args);
      },
    });
  };

  /**
   * 注册脚本菜单
   */
  const registerMenu = () => {
    let ui;

    GM_registerMenuCommand(`设置`, () => {
      if (commonui && commonui.mainMenuItems) {
        if (ui === undefined) {
          ui = new UI();

          new SystemModule(ui);
          new CustomModule(ui);
        }

        ui.render();
      }
    });
  };

  /**
   * 重新渲染
   * @param {Number | undefined} index 重新渲染的楼层,为空时重新渲染全部
   */
  const rerender = (index) => {
    if (commonui === undefined || commonui.postArg === undefined) {
      return;
    }

    if (index === undefined) {
      Object.keys(commonui.postArg.data).forEach((item) => {
        rerender(item);
      });
      return;
    }

    const argid = parseInt(index, 10);

    if (Number.isNaN(argid) || argid < 0) {
      return;
    }

    // TODO 需要优化

    const system = GM_getValue("system", []);
    const custom = GM_getValue("custom", []);

    const item = commonui.postArg.data[argid];

    const lite = item.lite;

    const uid = parseInt(item.pAid, 10) || 0;

    const posterInfo = lite
      ? item.uInfoC.closest("tr").querySelector(".posterInfoLine")
      : item.uInfoC;

    const container = item.pC.closest(".postbox");

    // 主容器样式
    container.classList.add("s-user-enhance");

    // 头像
    {
      const element = posterInfo.querySelector(".avatar");

      if (element) {
        element.setAttribute(
          "s-user-enhance-visible",
          system.includes("头像") === false
        );
      }
    }

    // 头衔
    {
      const element = posterInfo.querySelector("[name='honor']");

      if (element) {
        element.setAttribute(
          "s-user-enhance-visible",
          system.includes("头衔") === false
        );
      }
    }

    // 声望进度条
    {
      const element = posterInfo.querySelector(".r_container");

      if (element) {
        element.setAttribute(
          "s-user-enhance-visible",
          system.includes("声望") === false
        );
      }
    }

    // 声望、威望、级别、注册、发帖、财富
    {
      const elements = lite
        ? posterInfo.querySelectorAll(".usercol")
        : posterInfo.querySelectorAll(".stat NOBR");

      [...elements].forEach((element) => {
        if (lite) {
          ["声望", "威望", "级别", "注册", "发帖", "财富"].forEach((label) => {
            if (element.innerText.indexOf(label) >= 0) {
              element.innerHTML = element.innerHTML.replace(" · ", "");

              element.setAttribute(
                "s-user-enhance-visible",
                system.includes(label) === false
              );
            }
          });
        } else {
          const container = element.closest("DIV");

          container.style = "float: left; min-width: 50%;";

          ["声望", "威望", "级别", "注册", "发帖", "财富"].forEach((label) => {
            if (element.innerText.indexOf(label) >= 0) {
              container.setAttribute(
                "s-user-enhance-visible",
                system.includes(label) === false
              );
            }
          });
        }
      });
    }

    // 徽章
    {
      const anchor = posterInfo.querySelector("[name='medal']");

      if (anchor) {
        const br = anchor.nextElementSibling;
        const text = (() => {
          const previous =
            anchor.previousElementSibling || anchor.previousSibling;

          if (previous.nodeName === "SPAN") {
            return previous;
          }

          const span = document.createElement("SPAN");

          span.appendChild(previous);

          insertBefore(span, anchor);

          return span;
        })();

        const visible = system.includes("徽章") === false;

        if (lite) {
          text.innerHTML = text.innerHTML.replace(" · ", "");

          anchor
            .closest(".usercol")
            .setAttribute("s-user-enhance-visible", visible);
        } else {
          [text, anchor, br].forEach((element) => {
            element.setAttribute("s-user-enhance-visible", visible);
          });
        }
      }
    }

    // 版面
    {
      const anchor = posterInfo.querySelector("[name='site']");

      if (anchor) {
        const container = anchor.closest("SPAN");
        const br = container.nextElementSibling;

        const visible = system.includes("版面") === false;

        if (lite) {
          anchor
            .closest(".usercol")
            .setAttribute("s-user-enhance-visible", visible);
        } else {
          [container, br].forEach((element) => {
            if (element) {
              element.setAttribute("s-user-enhance-visible", visible);
            }
          });
        }
      }
    }

    // 备注
    {
      const elements = [
        ...posterInfo.querySelectorAll("SPAN[title^='公开备注']"),
        ...posterInfo.querySelectorAll("SPAN[title^='版主可见']"),
      ];

      [...elements].forEach((element) => {
        const container = element.closest("SPAN");

        container.setAttribute(
          "s-user-enhance-visible",
          system.includes("备注") === false
        );
      });
    }

    // 签名
    {
      const signC = item.signC;

      if (signC) {
        signC.setAttribute(
          "s-user-enhance-visible",
          system.includes("签名") === false
        );
      }
    }

    if (uid <= 0) {
      return;
    }

    // 粉丝
    {
      const element = (() => {
        const anchor = posterInfo.querySelector(
          "[name='s-user-enhance-follows']"
        );

        if (anchor) {
          return anchor;
        }

        const span = document.createElement("SPAN");

        span.setAttribute("name", `s-user-enhance-follows`);
        span.className = "small_colored_text_btn stxt block_txt_c2 vertmod";
        span.style.cursor = "default";
        span.style.margin = "0 0 0 4px";
        span.innerHTML = `
          <span class="white">
            <span style="font-family: comm_glyphs; -webkit-font-smoothing: antialiased; line-height: 1em;">★</span>
            <span name="s-user-enhance-follows-value"></span>
          </span>`;

        const uid = posterInfo.querySelector("[name='uid']");

        insertAfter(span, uid);

        return span;
      })();

      const value = element.querySelector(
        "[name='s-user-enhance-follows-value']"
      );

      const visible = custom.includes("粉丝");

      if (visible) {
        api.getUserInfo(uid).then(({ follow_by_num }) => {
          value.innerHTML = follow_by_num || 0;

          element.setAttribute("s-user-enhance-visible", true);
        });
      }

      element.setAttribute("s-user-enhance-visible", false);
    }

    // 点赞
    {
      const element = (() => {
        const anchor = posterInfo.querySelector(
          "[name='s-user-enhance-likes']"
        );

        if (anchor) {
          return anchor;
        }

        const span = document.createElement("SPAN");

        span.setAttribute("name", `s-user-enhance-likes`);
        span.className = "small_colored_text_btn stxt block_txt_c2 vertmod";
        span.style.cursor = "default";
        span.style.margin = "0 0 0 4px";
        span.innerHTML = `
          <span class="white">
            <span style="font-family: comm_glyphs; -webkit-font-smoothing: antialiased; line-height: 1em;">⯅</span>
            <span name="s-user-enhance-likes-value"></span>
          </span>`;

        const uid = posterInfo.querySelector("[name='uid']");

        insertAfter(span, uid);

        return span;
      })();

      const value = element.querySelector(
        "[name='s-user-enhance-likes-value']"
      );

      const visible = custom.includes("点赞");

      if (visible) {
        api.getUserInfo(uid).then(({ more_info }) => {
          const likes = Object.values(more_info || {}).find(
            (item) => item.type === 8
          );

          value.innerHTML = likes ? likes.data : 0;

          element.setAttribute("s-user-enhance-visible", true);
        });
      }

      element.setAttribute("s-user-enhance-visible", false);
    }

    // 坛龄
    {
      const element = (() => {
        const anchor = posterInfo.querySelector(
          "[name='s-user-enhance-regdays']"
        );

        if (anchor) {
          return anchor;
        }

        if (lite) {
          const span = document.createElement("SPAN");

          span.setAttribute("name", `s-user-enhance-regdays`);
          span.className = "usercol nobr";
          span.innerHTML = `坛龄 <span class="userval" name="s-user-enhance-regdays-value"></span>`;

          const lastChild = [...posterInfo.querySelectorAll(".usercol")].pop();

          insertAfter(span, lastChild);

          return span;
        }

        const div = document.createElement("DIV");

        div.setAttribute("name", `s-user-enhance-regdays`);
        div.style = "float: left; min-width: 50%";
        div.innerHTML = `
          <nobr>
            <span>坛龄: <span class="userval numericl" name="s-user-enhance-regdays-value"></span></span>
          </nobr>`;

        const lastChild = posterInfo.querySelector('.stat DIV[class="clear"]');

        insertBefore(div, lastChild);

        return div;
      })();

      const value = element.querySelector(
        "[name='s-user-enhance-regdays-value']"
      );

      const visible = custom.includes("坛龄");

      if (visible) {
        const { regdate } = commonui.userInfo.users[uid];

        const { years, months, days } = Tools.dateDiff(
          new Date(regdate * 1000)
        );

        value.title = ``;
        value.innerHTML = ``;

        [
          [years, "年"],
          [months, "月"],
          [days, "天"],
        ].forEach(([item, unit]) => {
          if (item > 0) {
            value.title += `${item}${unit}`;

            if (value.innerHTML.length === 0) {
              value.innerHTML = `${item}${unit}`;
            }
          }
        });

        if (value.innerHTML.length === 0) {
          value.innerHTML = `0天`;
        }
      }

      element.setAttribute("s-user-enhance-visible", visible);
    }

    // 离线
    {
      const element = (() => {
        const anchor = posterInfo.querySelector(
          "[name='s-user-enhance-offdays']"
        );

        if (anchor) {
          return anchor;
        }

        if (lite) {
          const span = document.createElement("SPAN");

          span.setAttribute("name", `s-user-enhance-offdays`);
          span.className = "usercol nobr";
          span.innerHTML = `离线 <span class="userval" name="s-user-enhance-offdays-value"></span>`;

          const lastChild = [...posterInfo.querySelectorAll(".usercol")].pop();

          insertAfter(span, lastChild);

          return span;
        }

        const div = document.createElement("DIV");

        div.setAttribute("name", `s-user-enhance-offdays`);
        div.style = "float: left; min-width: 50%";
        div.innerHTML = `
          <nobr>
            <span>离线: <span class="userval numericl" name="s-user-enhance-offdays-value"></span></span>
          </nobr>`;

        const lastChild = posterInfo.querySelector('.stat DIV[class="clear"]');

        insertBefore(div, lastChild);

        return div;
      })();

      const value = element.querySelector(
        "[name='s-user-enhance-offdays-value']"
      );

      const visible = custom.includes("离线");

      if (visible) {
        const thisvisit = commonui.userInfo.users[uid].thisvisit;
        const postTime = item.postTime;

        const time = Math.max(thisvisit, postTime) * 1000;

        const diff = new Date() - new Date(time);

        const start = new Date(2000, 0, 1);
        const end = new Date();

        end.setTime(start.getTime() + diff);

        const { years, months, days } = Tools.dateDiff(start, end);

        value.title = ``;
        value.innerHTML = ``;

        [
          [years, "年"],
          [months, "月"],
          [days, "天"],
        ].forEach(([item, unit]) => {
          if (item > 0) {
            value.title += `${item}${unit}`;

            if (value.innerHTML.length === 0) {
              value.innerHTML = `${item}${unit}`;
            }
          }
        });
      } else {
        value.innerHTML = ``;
      }

      element.setAttribute(
        "s-user-enhance-visible",
        value.innerHTML.length > 0
      );
    }

    // 发帖
    {
      const element = (() => {
        const anchor = posterInfo.querySelector(
          "[name='s-user-enhance-postnum']"
        );

        if (anchor) {
          return anchor;
        }

        if (lite) {
          const span = document.createElement("SPAN");

          span.setAttribute("name", `s-user-enhance-postnum`);
          span.className = "usercol nobr";
          span.innerHTML = `发帖 <span class="userval" name="s-user-enhance-postnum-value"></span>`;

          const lastChild = [...posterInfo.querySelectorAll(".usercol")].pop();

          insertAfter(span, lastChild);

          return span;
        }

        const div = document.createElement("DIV");

        div.setAttribute("name", `s-user-enhance-postnum`);
        div.style = "float: left; min-width: 50%";
        div.innerHTML = `
          <nobr>
            <span>发帖: <span class="userval numericl" name="s-user-enhance-postnum-value"></span></span>
          </nobr>`;

        const lastChild = posterInfo.querySelector('.stat DIV[class="clear"]');

        insertBefore(div, lastChild);

        return div;
      })();

      const value = element.querySelector(
        "[name='s-user-enhance-postnum-value']"
      );

      const visible = custom.includes("发帖");

      if (visible) {
        const { postnum, regdate } = commonui.userInfo.users[uid];

        const days = Math.ceil((Date.now() / 1000 - regdate) / (24 * 60 * 60));

        const postnumPerDay = postnum / days;

        value.title = `日均: ${postnumPerDay.toFixed(1)}`;
        value.innerHTML = postnum;
      }

      element.setAttribute("s-user-enhance-visible", visible);
    }

    // 属地
    {
      const element = (() => {
        const anchor = posterInfo.querySelector(
          "[name='s-user-enhance-ipLoc']"
        );

        if (anchor) {
          return anchor;
        }

        if (lite) {
          const span = document.createElement("SPAN");

          span.setAttribute("name", `s-user-enhance-ipLoc`);
          span.className = "usercol nobr";
          span.innerHTML = `<span class="userval" name="s-user-enhance-ipLoc-value"></span>`;

          const lastChild = [...posterInfo.querySelectorAll(".usercol")].pop();

          insertAfter(span, lastChild);

          return span;
        }

        const div = document.createElement("DIV");

        div.setAttribute("name", `s-user-enhance-ipLoc`);
        div.style = "float: left; min-width: 50%";
        div.innerHTML = `<span name="s-user-enhance-ipLoc-value"></span>`;

        const lastChild = posterInfo.querySelector('.stat DIV[class="clear"]');

        insertBefore(div, lastChild);

        return div;
      })();

      const value = element.querySelector(
        "[name='s-user-enhance-ipLoc-value']"
      );

      const visible = custom.includes("属地");

      if (visible) {
        api.getIpLocations(uid).then((data) => {
          if (data.length) {
            value.innerHTML = `${lite ? "属地 " : "属地: "}${data
              .map(
                ({ ipLoc, timestamp }) =>
                  `<span class="userval" title="${
                    timestamp ? commonui.time2dis(timestamp / 1000) : ""
                  }">${ipLoc}</span>`
              )
              .join(", ")}`;

            element.setAttribute("s-user-enhance-visible", true);
          }
        });
      }

      element.setAttribute("s-user-enhance-visible", false);
    }

    // 曾用名
    {
      const element = (() => {
        const anchor = posterInfo.querySelector(
          "[name='s-user-enhance-oldname']"
        );

        if (anchor) {
          return anchor;
        }

        if (lite) {
          const span = document.createElement("SPAN");

          span.setAttribute("name", `s-user-enhance-oldname`);
          span.className = "usercol nobr";
          span.innerHTML = `<span class="userval" name="s-user-enhance-oldname-value"></span>`;

          const lastChild = [...posterInfo.querySelectorAll(".usercol")].pop();

          insertAfter(span, lastChild);

          return span;
        }

        const div = document.createElement("DIV");

        div.setAttribute("name", `s-user-enhance-oldname`);
        div.style = "float: left; width: 100%";
        div.innerHTML = `<span name="s-user-enhance-oldname-value"></span>`;

        const lastChild = posterInfo.querySelector('.stat DIV[class="clear"]');

        insertBefore(div, lastChild);

        return div;
      })();

      const value = element.querySelector(
        "[name='s-user-enhance-oldname-value']"
      );

      const visible = custom.includes("曾用名");

      if (visible) {
        api.getUsernameChanged(uid).then((data) => {
          const values = Object.values(data || {});

          if (values.length) {
            value.innerHTML = `${lite ? "曾用名 " : "曾用名: "}${values
              .map(
                ({ username, time }) =>
                  `<span class="userval" title="${commonui.time2dis(
                    time
                  )}">${username}</span>`
              )
              .join(", ")}`;

            element.setAttribute("s-user-enhance-visible", true);
          }
        });
      }

      element.setAttribute("s-user-enhance-visible", false);
    }

    // 游戏档案
    {
      const element = (() => {
        const anchor = posterInfo.querySelector(
          "[name='s-user-enhance-games']"
        );

        if (anchor) {
          return anchor;
        }

        const div = document.createElement("DIV");

        div.setAttribute("name", `s-user-enhance-games`);
        div.style = "margin: 0 -2px;";
        div.innerHTML = ``;

        if (lite) {
          const lastChild = [...posterInfo.querySelectorAll(".usercol")].pop();

          insertAfter(div, lastChild);
        } else {
          const lastChild = posterInfo.querySelector(".stat").lastChild;

          insertBefore(div, lastChild);
        }

        return div;
      })();

      const visible = custom.includes("游戏档案");

      if (visible) {
        element.innerHTML = ``;

        api.getUserGameInfo(uid).then((info) => {
          // Steam
          if (info.steam) {
            const { steam_user_id, steam_user_name } = info.steam;

            const steam = (() => {
              if (steam_user_id) {
                const element = document.createElement("A");

                element.href = `https://steamcommunity.com/profiles/${steam_user_id}`;
                element.style = `
                  background-image: url(${unsafeWindow.__IMG_BASE}/misc/fid414/headline/icon03.png);
                  background-repeat: no-repeat;
                  background-position: 50% 50%;
                  background-size: contain;
                  width: 20px;
                  height: 20px;
                  display: inline-block;
                  cursor: pointer;
                  outline: none;`;
                element.title = `${steam_user_name}[${steam_user_id}]`;

                return element;
              }

              return null;
            })();

            if (steam) {
              steam.style.margin = "2px";
              element.appendChild(steam);
            }

            element.setAttribute("s-user-enhance-visible", true);
          }

          // PSN
          if (info.psn) {
            const { psn_user_id, psn_user_name } = info.psn;

            const psn = (() => {
              if (psn_user_name) {
                const element = document.createElement("A");

                element.href = `https://psnprofiles.com/${psn_user_name}`;
                element.style = `
                  background-image: url(${unsafeWindow.__IMG_BASE}/misc/fid414/headline/icon05.png);
                  background-repeat: no-repeat;
                  background-position: 50% 50%;
                  background-size: contain;
                  width: 20px;
                  height: 20px;
                  display: inline-block;
                  cursor: pointer;
                  outline: none;`;
                element.title = `${psn_user_name}[${psn_user_id}]`;

                return element;
              }

              return null;
            })();

            if (psn) {
              psn.style.margin = "2px";
              element.appendChild(psn);
            }

            element.setAttribute("s-user-enhance-visible", true);
          }

          // NS
          if (info.nintendo) {
            const { user_info } = info.nintendo;

            const nintendo = (() => {
              if (user_info) {
                const { ns_nickname, ns_friendcode } = user_info.user;

                const element = document.createElement("A");

                element.style = `
                  background-image: url(${unsafeWindow.__IMG_BASE}/misc/fid414/headline/icon01.png);
                  background-repeat: no-repeat;
                  background-position: 50% 50%;
                  background-size: contain;
                  width: 20px;
                  height: 20px;
                  display: inline-block;
                  cursor: pointer;
                  outline: none;`;
                element.title = `${ns_nickname}[${
                  ns_friendcode === "SW-XXXX-XXXX-XXXX" ? "-" : ns_friendcode
                }]`;

                return element;
              }

              return null;
            })();

            if (nintendo) {
              nintendo.style.margin = "2px";
              element.appendChild(nintendo);
            }

            element.setAttribute("s-user-enhance-visible", true);
          }

          // 刀塔
          if (info.steam) {
            const { steam_user_id } = info.steam;

            const stratz = (() => {
              if (steam_user_id && unsafeWindow.__CURRENT_GFID === 321) {
                const shortID = Number.isSafeInteger(steam_user_id)
                  ? steam_user_id
                  : Number(steam_user_id.substr(-16, 16)) - 6561197960265728;

                const element = document.createElement("A");

                element.href = `https://stratz.com/players/${shortID}`;
                element.style = `
                  background-image: url(${unsafeWindow.__IMG_BASE}/proxy/cache_attach/ficon/321u.png);
                  background-repeat: no-repeat;
                  background-position: 50% 50%;
                  background-size: contain;
                  width: 20px;
                  height: 20px;
                  display: inline-block;
                  cursor: pointer;
                  outline: none;`;
                element.title = shortID;

                return element;
              }

              return null;
            })();

            if (stratz) {
              stratz.style.margin = "2px";
              element.appendChild(stratz);
            }

            element.setAttribute("s-user-enhance-visible", true);
          }

          // 原神
          if (info.genshin) {
            const { userInfo } = info.genshin;

            const genshin = (() => {
              if (userInfo.ys_id) {
                const element = document.createElement("A");

                element.style = `
                  background-image: url(${unsafeWindow.__IMG_BASE}/proxy/cache_attach/ficon/650u.png);
                  background-repeat: no-repeat;
                  background-position: 50% 50%;
                  background-size: contain;
                  width: 20px;
                  height: 20px;
                  display: inline-block;
                  cursor: pointer;
                  outline: none;`;
                element.title = `${userInfo.nickname}[${userInfo.ys_id}]`;

                return element;
              }

              return null;
            })();

            if (genshin) {
              genshin.style.margin = "2px";
              element.appendChild(genshin);
            }

            element.setAttribute("s-user-enhance-visible", true);
          }

          // 深空之眼
          if (info.skzy) {
            const { skzy_uid, nick_name } = info.skzy;

            const skzy = (() => {
              if (skzy_uid) {
                const element = document.createElement("A");

                element.style = `
                  background-image: url(${unsafeWindow.__IMG_BASE}/proxy/cache_attach/ficon/848u.png);
                  background-repeat: no-repeat;
                  background-position: 50% 50%;
                  background-size: contain;
                  width: 20px;
                  height: 20px;
                  display: inline-block;
                  cursor: pointer;
                  outline: none;`;
                element.title = `${nick_name}[${skzy_uid}]`;

                return element;
              }

              return null;
            })();

            if (skzy) {
              skzy.style.margin = "2px";
              element.appendChild(skzy);
            }

            element.setAttribute("s-user-enhance-visible", true);
          }
        });
      }

      element.setAttribute("s-user-enhance-visible", false);
    }

    // 刀塔段位
    {
      const element = (() => {
        const anchor = posterInfo.querySelector(
          "[name='s-user-enhance-dota-rank']"
        );

        if (anchor) {
          return anchor;
        }

        const div = document.createElement("DIV");

        div.setAttribute("name", `s-user-enhance-dota-rank`);
        div.style = "margin: 2px 0";
        div.innerHTML = ``;

        if (lite) {
          return null;
        }

        const lastChild = posterInfo.querySelector(".stat");

        insertAfter(div, lastChild);

        return div;
      })();

      if (element) {
        const visible = custom.includes("刀塔段位");

        if (visible) {
          element.innerHTML = ``;

          api.getSteamInfo(uid).then(async ({ steam_user_id }) => {
            if (steam_user_id) {
              const shortID = Number.isSafeInteger(steam_user_id)
                ? steam_user_id
                : Number(steam_user_id.substr(-16, 16)) - 6561197960265728;

              // TODO 代码优化
              // 简单的缓存,同一个人每天只请求一次
              const data = (await cache.get("DotaRank")) || {};

              const info = await new Promise((resolve) => {
                if (data[shortID]) {
                  const { timestamp } = data[shortID];

                  const date = new Date(timestamp);

                  const isToday = Tools.dateIsToday(date);

                  if (isToday) {
                    resolve(data[shortID]);
                    return;
                  }

                  delete data[shortID];
                }

                fetch(`https://api.opendota.com/api/players/${shortID}`)
                  .then((res) => res.json())
                  .then((res) => {
                    if (res) {
                      data[shortID] = {
                        ...res,
                        timestamp: new Date().getTime(),
                      };

                      cache.put("DotaRank", data);

                      resolve(res);
                      return;
                    }

                    resolve(null);
                  })
                  .catch(() => {
                    resolve(null);
                  });
              });

              if (info.profile) {
                const { rank_tier, leaderboard_rank } = info;

                const medals = [
                  "先锋",
                  "卫士",
                  "中军",
                  "统帅",
                  "传奇",
                  "万古流芳",
                  "超凡入圣",
                  "冠绝一世",
                ];

                const medal = Math.floor(rank_tier / 10);

                const star = rank_tier % 10;

                element.innerHTML = `
                  <div style="
                    width: 64px;
                    height: 64px;
                    display: inline-flex;
                    -webkit-box-pack: center;
                    justify-content: center;
                    -webkit-box-align: center;
                    align-items: center;
                    position: relative;
                    font-size: 10px;
                    overflow: hidden;
                  " title="${
                    medals[medal - 1]
                      ? `${medals[medal - 1]}[${leaderboard_rank || star}]`
                      : ""
                  }">
                    <svg viewBox="0 0 256 256" style="max-width: 256px; max-height: 256px">
                      <image href="https://cdn.stratz.com/images/dota2/seasonal_rank/medal_${medal}.png" height="100%" width="100%"></image>
                      ${
                        star > 0
                          ? `<image href="https://cdn.stratz.com/images/dota2/seasonal_rank/star_${star}.png" height="100%" width="100%"></image>`
                          : ""
                      }
                    </svg>
                    ${
                      leaderboard_rank
                        ? `<div style="
                            background-color: rgba(0, 0, 0, 0.7);
                            border-radius: 4px;
                            color: rgba(255, 255, 255, 0.8);
                            padding: 0.2em 0.3em 0.3em;
                            position: absolute;
                            line-height: normal;
                            bottom: 0;
                            ">${leaderboard_rank}</div>`
                        : ""
                    }
                  </div>`;

                element.setAttribute("s-user-enhance-visible", true);
              }
            }
          });
        }

        element.setAttribute("s-user-enhance-visible", false);
      }
    }
  };

  /**
   * 插入至元素之前
   * @param {HTMLElement} element 新元素
   * @param {HTMLElement} target  目标元素
   */
  const insertBefore = (element, target) => {
    const parentNode = target.parentNode;

    parentNode.insertBefore(element, target);
  };

  /**
   * 插入至元素之后
   * @param {HTMLElement} element 新元素
   * @param {HTMLElement} target  目标元素
   */
  const insertAfter = (element, target) => {
    const parentNode = target.parentNode;

    if (parentNode.lastChild === target) {
      parentNode.appendChild(element);
      return;
    }

    parentNode.insertBefore(element, target.nextSibling);
  };

  // 主函数
  (async () => {
    // 初始化缓存和 API 并绑定
    const libs = initCacheAndAPI();

    cache = libs.cache;
    api = libs.api;

    // 注册脚本菜单
    registerMenu();

    // 处理 commonui 模块
    if (unsafeWindow.commonui) {
      handleCommonui(unsafeWindow.commonui);
      return;
    }

    Tools.interceptProperty(unsafeWindow, "commonui", {
      afterSet: (value) => {
        handleCommonui(value);
      },
    });
  })();
})();