FBI Open the door! B站评论区用户转发动态统计

统计B站评论区内用户转发动态的情况,按照原动态UP主分类。

// ==UserScript==
// @name        FBI Open the door! B站评论区用户转发动态统计
// @namespace   lightyears.im
// @version     1.2.3
// @description 统计B站评论区内用户转发动态的情况,按照原动态UP主分类。
// @author      1MLightyears
// @match       *://www.bilibili.com/video/*
// @match       *://www.bilibili.com/read/*
// @match       *://t.bilibili.com/*
// @match       *://space.bilibili.com/*
// @icon        https://static.hdslb.com/images/favicon.ico
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// @grant       GM_info
// @connect     api.bilibili.com
// @license     Apache Licence 2.0
// @run-at      document-end
// ==/UserScript==

/*
本脚本的创意来自于 原神玩家指示器(https://greasyfork.org/zh-CN/scripts/450720-%E5%8E%9F%E7%A5%9E%E7%8E%A9%E5%AE%B6%E6%8C%87%E7%A4%BA%E5%99%A8)
感谢 laupuz_xu(https://greasyfork.org/zh-CN/users/954434-laupuz-xu)!
GitHub: https://github.com/1MLightyears/FBIOpenTheDoor
*/


(function () {
  "use strict";

  function genUuid() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
      let r = (Math.random() * 16) | 0,
        v = c == 'x' ? r : (r & 0x3) | 0x8;
      return v.toString(16);
    });
  }

  const bilibiliVersion = !document.querySelector(".bili-dyn-version-control");  // 新旧版本flag
  const CLASS_BannerDOM = "FO-banner";  // 成分条class
  const CLASS_StatDOM = "FO-stat";  // 成分条里每个成分class
  const CLASS_Gateway = "FO-gateway";  // 入口class
  const CLASS_ColleDOM = "FO-colle";  // 自定义集class
  const CLASS_SubDOM = "FO-sub-stat";  // 自定义集中组分class
  const CLASS_UPiine = "reply-tags";  // "up主觉得很赞"class
  const CLASS_B_ban = "van-icon-info_prohibit";  // 禁止图标class
  const A_User = "FO-user";  // 已经标注查成分的用户
  const QS_BannerInsertBefore_new = "div.root-reply, div.sub-reply-info";  // 新版下banner应插入至此DOM前
  const localStorageKey = "FBIOpenTheDoor";  // 使用的localStorage的键名
  const removeIconCode = "2718";  // 删除功能的图标utf-8码
  const URL_basic = "t.bilibili.com";  // 跨域时的基准自定义集定义所在的网址
  const sync_collections = window.location.host !== URL_basic; // 是否同步的自定义集定义

  let A_Uid, QS_MainCommentUserHeader, QS_ReplyUserHeader, QS_Uid, QS_NewUser, QS_ToolbarDOM;
  if (bilibiliVersion) {
    // 新版
    QS_MainCommentUserHeader = "div.root-reply-container div.user-info";  // 评论的用户行DOM
    QS_ReplyUserHeader = "div.sub-reply-item > div.sub-user-info";  // 楼中楼用户行DOM
    QS_Uid = "div[data-user-id]";  // 用户名DOM
    QS_NewUser = `div.reply-item:not([${A_User}]), div.sub-reply-item:not([${A_User}])`;  // 新刷出来的用户DOM
    QS_ToolbarDOM = `div.reply-info, div.sub-reply-info`;  // 评论下赞、踩、回复工具栏DOM
    A_Uid = "data-user-id";  // 用户Uid属性
  } else {
    // 旧版
    QS_MainCommentUserHeader = "div.con > div.user";  // 评论的用户行DOM
    QS_ReplyUserHeader = "div.reply-con > div.user";  // 楼中楼用户行DOM
    QS_Uid = "a.name";  // 用户名DOM
    QS_NewUser = `div.reply-wrap:not([${A_User}])`;  // 新刷出来的用户DOM
    QS_ToolbarDOM = `div.info`;  // 评论下赞、踩、回复工具栏DOM
    A_Uid = "data-usercard-mid";  // 用户Uid属性
  }


  let CSSSheet = `
span.${CLASS_StatDOM}, span.${CLASS_ColleDOM} {
  text-align: center;
  align-self: center;
  margin: 2px;
  height: calc(2em);
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: pre;
  transition: all 0.5s, ease-in-out;
}

span.${CLASS_ColleDOM} {
  border-radius: 1rem;
}

span.${CLASS_StatDOM}:hover, span.${CLASS_ColleDOM}:hover {
  z-index: 256;
}

/* 成分条部分 */
span.${CLASS_StatDOM}:hover a{
  text-decoration: underline;
}

/* 自定义集部分 */
span.${CLASS_ColleDOM}>ul {
  position: absolute;
  top: auto;
  opacity: 0;
  transform: translateX(-50%);
  background-color: inherit;
  border-left: 5px solid white;
  border-right: 5px solid white;
  border-radius: 5px;
  box-shadow: 0px 0px 5px grey;
  transition: all 0.5s ease-in-out;
}

span.${CLASS_ColleDOM}:hover>ul, span.${CLASS_ColleDOM}>ul:hover{
  opacity: 1;
}

span.${CLASS_ColleDOM} li{
  background-color: inherit;
  border: 2px solid white;
  text-align: center;
  border-top: 3px solid white;
  border-bottom: 3px solid white;
  padding: 3px 1.5rem 3px 1.5rem;
}

span.${CLASS_ColleDOM} li:hover>a{
  text-decoration: underline !important;
}

span.${CLASS_ColleDOM} li>i{
  display: inline-block;
  position: absolute;
  right: 0;
  cursor: pointer;
  font-size: 1rem;
  margin-right: 2px;
  color: #fd676f;
  width: 1rem;
  opacity: 0;
}

span.${CLASS_ColleDOM} li>i:before{
  content: "\\${removeIconCode}";
}

span.${CLASS_ColleDOM} li:hover>i{
  opacity: 1;
}

/* banner部分 */
div.${CLASS_BannerDOM} {
  display: flex;
  overflow: hidden;
  padding: 2px;
  box-shadow: 0px 0px 5px gray;
  border-radius: 5px;
  text-shadow: 0px 0px 2px white;
}
`;
  // 为不同版本适配不同的CSS样式
  if (bilibiliVersion) {
    // 新版
    CSSSheet += `
a.${CLASS_Gateway} {
  display: none;
  color: grey;
  position: inherit;
  z-index: 128;
  font-weight: 700;
  margin-left: 2em;
}

div.root-reply-container:hover a.${CLASS_Gateway},
div.sub-reply-item:hover a.${CLASS_Gateway} {
  display: inline;
}

/* 干掉挡事的认证粉丝牌 */
.reply-decorate:hover {
  display: none;
}
`;
  } else {
    // 旧版
    CSSSheet += `
a.${CLASS_Gateway} {
  display: none;
  color: grey;
  position: inherit;
  z-index: 128;
  font-weight: 700;
}

div.con:hover>:not(.reply-box) a.${CLASS_Gateway},
div.con div.reply-item:hover a.${CLASS_Gateway} {
  display: inline;
}

/* 干掉挡事的认证粉丝牌 */
.sailing:hover {
  display: none;
}`;
  }

  // 初始化成分条反查自定义集cid
  let stat2Collection = {};

  // dataTransfer不能存取对象?
  let _dragDOM = null;

  let iframe_t;

  // 在customCollections中的自定义集记录的数据结构:
  // {
  //    "cid": <uuid>
  //    "name": <str>
  //    "contains": [<list of uid>]
  // }
  let customCollections = JSON.parse(window.localStorage.getItem(localStorageKey) || '{ "collections": { } }').collections;


  // 颜色盘
  const pallete = [
    "Pink",
    "LightSkyBlue",
    "Aqua",
    "SpringGreen",
    "DarkSeaGreen",
    "Beige",
    "Gold",
    "Wheat",
    "Tan",
    "DarkSalmon",
    "Tomato",
    "Silver",
  ];

  // 评论类型
  const TComment = {
    MainComment: 0,  // 评论区评论
    ReplyComment: 1,  // 评论区回复楼中楼评论
  };

  // 跨域信息类型
  const TCOM = {
    UPDATE_COLLECTIONS: 0,  // data字段为携带的customCollections
    FETCH_COLLECTIONS: 1,
  };

  function updateStat2Collection() {
    //// 修改自定义集记录后关联修改反向索引
    for (let c in customCollections) {
      for (let i = 0, l = customCollections[c].contains.length; i < l; i++) {
        stat2Collection[customCollections[c].contains[i]] = c;
      }
    }
  }

  function saveCollection() {
    //// 保存自定义集设定至localStorage
    if (sync_collections) {
      iframe_t.contentWindow.postMessage({
        COM: TCOM.UPDATE_COLLECTIONS,
        data: customCollections,
      }, iframe_t.src);
    }
    let t = JSON.stringify({ "collections": customCollections });
    window.localStorage.setItem(localStorageKey, t);
    updateStat2Collection();
  }

  function createCollection() {
    //// 新建一个自定义集并记录
    let newCollection = {
      "cid": genUuid(),
      "name": "",
      "contains": [],
    };
    customCollections[newCollection.cid] = newCollection;
    return newCollection;
  }

  function removeCollection(collection) {
    if (!!customCollections[collection.cid]) {
      delete customCollections[collection.cid];
        return true;
    } else {
        return false;
    }
  }
  function mergeCollection(targetCollection, ...collections) {
    //// 将一些自定义集并入targetCollection自定义集
    // name为targetCollection的,count为所有collections的count之和再加targetCollection的
    for (let i = 0; i < collections.length; i++) {
      let co = collections[i];
      for (let j = 0; j < co.contains.length; j++) {
        if (!targetCollection.contains.includes(co.contains[j])) {
          targetCollection.contains.push(co.contains[j]);
          stat2Collection[co.contains[j]] = targetCollection.cid;
        }
      }
      delete customCollections[co.cid];
    }
    saveCollection();
  }
  function add2Collection(cid, uid) {
    ///// 将一个成分条(uid)编入一个自定义集(cid)
    let collection = customCollections[cid];
    if (!collection.contains.includes(uid)) {
      collection.contains.push(uid);
      stat2Collection[uid] = cid;
    }
    saveCollection();
  }
  function removeFromCollection(uid) {
    //// 将一个子成分条移出自定义集
    if (!stat2Collection.hasOwnProperty(uid)) return -1;
    let collection = customCollections[stat2Collection[uid]];
    collection.contains.splice(collection.contains.indexOf(uid), 1);
    if (collection.contains.length === 0) delete customCollections[stat2Collection[uid]];
    delete stat2Collection[uid];
    saveCollection();
    return 0;
  }
  function renameCollection(collection, newName) {
    //// 重命名一个自定义集为newName。
    // 解决重名冲突。若重名则返回false,否则返回重复的自定义集的uid。
    let repeated = "";
    for (let i in customCollections) {
      let c = customCollections[i];
      if (c.name === newName) {
        repeated = c;
        break;
      }
    }
    if (!repeated) {
      collection.name = newName;
      return 0;
    } else return repeated;
  }

  function renderAll() {
    //// 渲染当前页面上所有banner
    for (let i = 0, l = users.length; i < l; i++) {
      if (users[i].total) {
        setTimeout(users[i].render.bind(users[i]), 0);
      }
    }
  }

  class TDisp {
    //// banner中的显示块类
    constructor(id, name, count, stat_type, parent_banner) {
      this.dom = document.createElement("span");
      this.dom.disp_of = this;
      this.id = id || genUuid();
      this.name = name || "新集合";
      this.count = count || 0;
      this.stat_type = stat_type || TDisp.DispType.Statistic;
      this.parent_banner = parent_banner;
    }
    setName(newname) {
      this.name = newname.toString();
      if (this.dom.children.length && this.dom.childNodes[0].nodeType === 3) {
        // 仅修改文本
        this.dom.childNodes[0].nodeValue = this.name;
      } else {
        this.dom.innerHTML = this.name;
      }
      return this;
    }
    getName() {
      if (this.dom.children.length && this.dom.childNodes[0].nodeType === 3) {
        return this.dom.childNodes[0].nodeValue;
      } else {
        return this.dom.innerText;
      }
      return this;
    }
    appendChild(DispDOM) {
      let subDOM = document.createElement("span");
      subDOM._id = DispDOM.id;
      subDOM._name = DispDOM.name;
      subDOM._count = DispDOM.count;
      let percent = subDOM._count / this.total * 100;

      subDOM.classList.add(CLASS_SubDOM);

      let innerDetailDOM = document.createElement("a");
      innerDetailDOM.setAttribute("target", "_blank");
      innerDetailDOM.setAttribute("href", `//space.bilibili.com/${subDOM._id}`);
      innerDetailDOM.setAttribute("draggable", false);
      innerDetailDOM.innerText = subDOM._name;
      subDOM.appendChild(innerDetailDOM);
      subDOM.innerHTML += `(${subDOM._count}, ${Math.floor(percent)}%)`;

      this.dom.appendChild(subDOM);
      return this;
    }
    render(total, components) {
      //// 渲染显示块本身

      // common
      let percent = this.count / total * 100;
      this.dom.style.width = `${percent}%`;  // 宽度与数量成比例

      this.dom.addEventListener("mouseover", () => {
        if (percent < 99) {
          setTimeout(() => {
            this.dom.style.width = `max(calc(${percent}%), calc(${this.getName().length + 2}em))`;  // 显示所有的字,为数字和半角括号增加冗余空间
          }, 0);
        }
      });
      this.dom.addEventListener("dragover", (e) => {
        e.preventDefault();
      });
      this.dom.addEventListener("dragenter", (e) => {
        this.dom.style.boxShadow = "0px 0px 0.5em grey";
      });
      this.dom.addEventListener("dragleave", (e) => {
        this.dom.style.boxShadow = "";
      });

      // 修饰每个成分,根据类型
      switch (this.stat_type) {
        case TDisp.DispType.Statistic:
          // 成分条
          this.dom.classList.add(CLASS_StatDOM);
          this.dom.setAttribute("draggable", "true");

          // up主链接
          let innerDetailDOM = document.createElement("a");
          innerDetailDOM.setAttribute("target", "_blank");
          innerDetailDOM.setAttribute("href", `//space.bilibili.com/${this.id}`);
          innerDetailDOM.setAttribute("draggable", false);
          innerDetailDOM.innerText = this.name;
          this.dom.appendChild(innerDetailDOM);
          this.dom.innerHTML += `(${this.count}, ${Math.floor(percent)}%)`;

          this.dom.addEventListener("mouseleave", () => {
            this.dom.style.width = `${percent}%`;
          });
          this.dom.addEventListener("dragstart", (e) => {
            _dragDOM = this.dom;
            while (!(_dragDOM instanceof HTMLSpanElement && _dragDOM.hasOwnProperty("disp_of"))) {
              _dragDOM = _dragDOM.parentNode;
            }
          });
          this.dom.addEventListener("drop", (e) => {
            this.dom.style.boxShadow = "";
            while (!(_dragDOM instanceof HTMLSpanElement && _dragDOM.hasOwnProperty("disp_of"))) {
              _dragDOM = _dragDOM.parentNode;
            }

            let targetCollection = createCollection();

            if (_dragDOM.disp_of.id !== this.id) {
              add2Collection(targetCollection.cid, this.id);
              add2Collection(targetCollection.cid, _dragDOM.disp_of.id);
            } else if (confirm(`要将【${this.name}】单独编入一个自定义集吗?`)) {
              // 自己落自己时询问用户
              add2Collection(targetCollection.cid, this.id);
            } else {
               removeCollection(targetCollection);
             }
            renderAll();
            setTimeout(() => {
              for (let i = 0, l = this.parent_banner.statics.length; i < l; i++) {
                if (this.parent_banner.statics[i].id === targetCollection.cid) {
                  this.parent_banner.statics[i].dom.dispatchEvent(new Event("dblclick"));
                  break;
                }
              }
            }, 100);
          });
          innerDetailDOM.ondragstart =
            innerDetailDOM.ondragenter =
            innerDetailDOM.ondragleave =
            innerDetailDOM.ondragend = (e) => {
              // 防止打开链接
              e.preventDefault();
              e.stopPropagation();
              this.dom.dispatchEvent(e);
            };
          break;
        case TDisp.DispType.Collection:
          // 渲染自定义集
          this.dom.classList.add(CLASS_ColleDOM);
          this.setName(`${this.name}(${this.count}, ${Math.floor(percent)}%)`);
          let renameInputDOM = document.createElement("input");
          renameInputDOM.placeholder = customCollections[this.id].name || "新集合";
          renameInputDOM.style.display = "none";
          renameInputDOM.style.width = `${renameInputDOM.placeholder.length + 2}rem`;
          renameInputDOM.onkeyup = (e) => { if (e.type === "keyup" && e.key === "Enter") { e.target.blur(); } };
          renameInputDOM.onblur = (e) => {
            this.name = renameInputDOM.value || renameInputDOM.placeholder;
            // 检查有无重名的自定义集
            let renret;
            for (; ;) {
              renret = renameCollection(customCollections[this.id], this.name);
              if (!!renret) {
                if (confirm(`已经有名为【${this.name}】的自定义集。是否合并它们?\n\n选择取消将建立名为【${this.name}*】的新自定义集。`)) {
                  mergeCollection(customCollections[renret.cid], customCollections[this.id]);
                  renderAll();
                  return;
                } else {
                  this.name += "*";
                }
              } else break;
            }
            customCollections[this.id].name = this.name;
            this.setName(`${this.name}(${this.count}, ${Math.floor(percent)}%)`);
            saveCollection();
            renameInputDOM.style.display = "none";
            renderAll();
          };
          this.dom.appendChild(renameInputDOM);

          let ul = document.createElement("ul");
          for (let i = 0, l = (components || []).length; i < l; i++) {
            let subStat = components[i],
              li = document.createElement("li"),
              innerDetailDOM = document.createElement("a"),
              innerPercent = subStat.count / total * 100;
            innerDetailDOM.setAttribute("target", "_blank");
            innerDetailDOM.setAttribute("href", `//space.bilibili.com/${subStat.uid}`);
            innerDetailDOM.setAttribute("draggable", false);
            innerDetailDOM.innerText = subStat.name;
            li.appendChild(innerDetailDOM);
            li.innerHTML += `(${subStat.count}, ${Math.floor(innerPercent)}%)`;
            let removeLiI = document.createElement("i");
            removeLiI.classList.add(CLASS_B_ban);
            removeLiI.addEventListener("click", () => {
              // 将当前子成分条的移除出当前自定义集
              removeFromCollection(subStat.uid);
              renderAll();
            });
            li.appendChild(removeLiI);

            ul.appendChild(li);
          }
          this.dom.appendChild(ul);

          let bannerLeft = this.parent_banner.bannerDOM.getBoundingClientRect().x;
          this.dom.addEventListener("mouseover", () => {
            // 自定义集显示包含成分,弹出位置
            let parentWidth = Number(/[0-9\.]+/.exec(getComputedStyle(this.dom).width)[0]);
            let parentLeft = this.dom.getBoundingClientRect().x - bannerLeft;
            this.dom.querySelector("ul").style.left = `${parentLeft + Math.floor(parentWidth / 2)}px`;
          });
          this.dom.addEventListener("mouseleave", () => {
            this.dom.style.width = `${percent}%`;
          });
          this.dom.addEventListener("drop", (e) => {
            this.dom.style.boxShadow = "";
            while (!(_dragDOM instanceof HTMLSpanElement && _dragDOM.hasOwnProperty("disp_of"))) {
              _dragDOM = _dragDOM.parentNode;
            }
            let targetCollection = customCollections[this.id];
            add2Collection(targetCollection.cid, _dragDOM.disp_of.id);
            renderAll();
          });
          this.dom.addEventListener("dblclick", (e) => {
            this.setName("");
            renameInputDOM.style.display = "inline-block";
            renameInputDOM.focus();
          });
          break;
      }
      return this;
    }
  }
  TDisp.DispType = {
    Statistic: 0,
    Collection: 1,
  };

  class TBilibiliUser {
    //// 评论用户类
    constructor(commentDOM) {
      //// 初始化用户类

      this.commentDOM = commentDOM;
      this.toolbarDOM = this.locateToolbar();
      this.userHeaderDOM = this.locateUserHeader();
      let userADOM = this.userHeaderDOM.querySelector(QS_Uid);
      this.uid = userADOM.getAttribute(A_Uid);
      this.name = userADOM.text;
      this.forwardCounter = {};
      this.bannerDOM = document.createElement("div");
      this.statics = [];
      this.offset = null;
      this.total = 0;
      this.multi_query = false;

      this.gatewayDOM = this.createGateway();
      if (bilibiliVersion) {
        // 新版
        let parentDOM = this.userHeaderDOM.parentNode;
        parentDOM.insertBefore(this.bannerDOM, parentDOM.querySelector(QS_BannerInsertBefore_new));
      } else {
        // 旧版
        this.userHeaderDOM.appendChild(this.bannerDOM);
      }
      this.commentDOM.setAttribute(A_User, true);
    }
    locateToolbar() {
      //// 定位评论工具栏
      let toolbarDOM = this.commentDOM.querySelector(QS_ToolbarDOM);
      return toolbarDOM;
    }
    locateUserHeader() {
      //// 定位用户行,确定评论类型
      let userHeaderDOM = this.commentDOM.querySelector(QS_MainCommentUserHeader);
      this.commentType = TComment.MainComment;
      if (!userHeaderDOM) {
        userHeaderDOM = this.commentDOM.querySelector(QS_ReplyUserHeader);
        this.commentType = TComment.ReplyComment;
      }
      return userHeaderDOM;
    }
    createGateway() {
      //// 修饰查成分入口
      let gatewayDOM = document.createElement("a");
      gatewayDOM.classList.add(CLASS_Gateway);
      gatewayDOM.innerHTML = "开门!查成分!";
      gatewayDOM.onclick = this.getForwards.bind(this);
      gatewayDOM.ondblclick = () => {
        this.multi_query = true;
        this.getForwards();
      };

      // "up主觉得很赞"是div block,移动顺序
      let upiineDOM = null;
      if (!!this.toolbarDOM.lastChild && this.toolbarDOM.lastChild.classList.contains(CLASS_UPiine)) {
        upiineDOM = this.toolbarDOM.lastChild;
        this.toolbarDOM.removeChild(upiineDOM);
      }
      this.toolbarDOM.appendChild(gatewayDOM);
      if (!!upiineDOM) {
        this.toolbarDOM.appendChild(upiineDOM);
      }

      return gatewayDOM;
    }
    render() {
      //// 渲染banner

      this.collect();

      // 统计并修饰入口链接
      this.total = 0;
      for (let i in this.forwardCounter) {
        this.total += this.forwardCounter[i].count;
      }
      this.gatewayDOM.text = `(已查询到${this.total}条) `;
      if (this.has_more) {
        if (this.multi_query) {
          setTimeout(this.getForwards(), Math.random() * 100);
        } else {
          this.gatewayDOM.text += "继续查!( ‘·A·’)";
        }
      } else {
        this.gatewayDOM.onclick = null;
        this.gatewayDOM.ondblclick = null;
      }
      this.bannerDOM.innerHTML = "";

      // 构建内部成分表 先渲染所有的成分条
      this.statics = [];
      for (let key in this.forwardCounter) {
        let curr = this.forwardCounter[key];
        this.statics.push(new TDisp(
          curr.uid || curr.cid,
          curr.name,
          curr.count,
          curr.stat_type,
          this
        ).render(this.total, curr.components));
      }

      // 显示statDOMs,刷新banner 数量较大的成分优先
      this.statics.sort((a, b) => {
        return b.count - a.count;
      });
      let colorNo = -1;
      for (let i = 0; i < this.statics.length; i++) {
        colorNo = colorNo + 1 < pallete.length ? colorNo + 1 : 0;
        this.bannerDOM.appendChild(this.statics[i].dom);
        this.statics[i].dom.style.backgroundColor = pallete[colorNo];
      }
      this.bannerDOM.className = CLASS_BannerDOM;
    }
    fetchHomepage() {
      //// 拿B站API url
      return `https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?&host_mid=${this.uid}` + (!!this.offset ? `&offset=${this.offset}` : "");
    }
    getForwards() {
      //// XHR拿动态列表
      this.gatewayDOM.text = "/// 警方突击中 ///";
      GM_xmlhttpRequest({
        method: "get",
        url: this.fetchHomepage(),
        data: "",
        headers: {
          'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'
        },
        onload: function (resp) {
          if (resp.status === 200) {
            if (!resp.data) {
              this.gatewayDOM.text=`受到流量控制||( -_-) 请稍后再试`;
            }
            resp = JSON.parse(resp.response);
            for (let i = 0; i < resp.data.items.length; i++) {
              let item = resp.data.items[i];
              if (item.hasOwnProperty("orig")) {
                let originalUpUid = item.orig.modules.module_author.mid;
                if (!originalUpUid) continue;
                if (this.forwardCounter.hasOwnProperty(originalUpUid)) {
                  this.forwardCounter[originalUpUid].count += 1;
                } else {
                  this.forwardCounter[originalUpUid] = {
                    "name": item.orig.modules.module_author.name,
                    "uid": item.orig.modules.module_author.mid,
                    "count": 1,
                    "stat_type": TDisp.DispType.Statistic
                  };
                }
              }
            }
            this.offset = resp.data.offset;
            this.has_more = resp.data.has_more;
            this.render();
              return true;
          } else {
            console.warn(`获取失败@uid=${this.uid}: status=${resp.status}`);
            return false;
          }
        }.bind(this)
      });
    }
    collect() {
      //// 根据自定义集分组情况,修改统计结果forwardCounter

      // 先将每个成分从自定义集中拆出来
      for (let key in this.forwardCounter) {
        let curr = this.forwardCounter[key];
        if (curr.stat_type === TDisp.DispType.Collection) {
          for (let i = curr.components.length - 1; i >= 0; i--) {
            let subStat = curr.components[i];
            let rawStat = this.forwardCounter[subStat.uid] ||
            // 在forwardCounter中的成分实例的数据结构
            /*
            {
              "name": (str)up名,
              "uid": (str)uuid,
              "count": (int)该up被转发的次数,
              "stat_type": (TDisp.DispType)对于成分条,该字段值为0(i.e. Statistic)
            };
            */
            {
              "name": subStat.name,
              "uid": subStat.uid,
              "count": 0,
              "stat_type": subStat.stat_type,
            };
            rawStat.count += subStat.count;
            this.forwardCounter[subStat.uid] = rawStat;
          }
          delete this.forwardCounter[key];
        }
      }

      // 再全部重新分组
      for (let key in this.forwardCounter) {
        let curr = this.forwardCounter[key];
        if (curr.stat_type === TDisp.DispType.Statistic && stat2Collection.hasOwnProperty(curr.uid)) {
          let cid = stat2Collection[curr.uid];
          // 在forwardCounter中的自定义集实例的数据结构
          /*
          {
            "name": (str)自定义集名,
            "cid": (str)uuid,
            "count": (int)子成分数量和,
            "components": (array of stat)原先在forwardCounter中的成分实例
            "stat_type": (TDisp.DispType)对于自定义集,该字段的值为1(i.e. Collection)
          }
          */
          let targetCollection = this.forwardCounter[cid] || {
            "name": customCollections[cid].name,
            "cid": cid,
            "count": 0,
            "components": [],
            "stat_type": TDisp.DispType.Collection,
          };
          targetCollection.components.push(curr);
          targetCollection.count += curr.count;
          this.forwardCounter[cid] = targetCollection;
          this.forwardCounter[cid].name = customCollections[cid].name;  // 补刷name
          delete this.forwardCounter[key];
        }
      }
    }
  }

  //// 从localStorage获取自定义集记录。
  // 20221126 FIX: t.bilibili.com 和 www.bilibili.com 中间存在跨域问题,localStorage不同步
  // sync_collections(bool): 是否从URL_basic获取自定义集记录
  if (!sync_collections) {
    function handleCollections(e) {
      //// URL_basic页面的窗口事件响应
      let origin = e.origin || e.originalEvent.origin;
      if (new RegExp("bilibili.com").exec(origin)) {
        switch (e.data.COM) {
          case TCOM.UPDATE_COLLECTIONS:
            customCollections = e.data.data || {};
            saveCollection();
            break;
          case TCOM.FETCH_COLLECTIONS:
            e.source.postMessage({
              COM: TCOM.UPDATE_COLLECTIONS,
              data: customCollections
            }, origin);
            break;
        }
      }
    }
    window.addEventListener("message", handleCollections, false);

    updateStat2Collection();
    console.log(`%c开门!\n查成分!\n%c(v${GM_info.script.version})`,
      `font-size: 2.5em;font-style:italic;color:gold`,
      `font-size:1em;color:cyan;font-style:italic`);
  } else {
    function recvCollections(e) {
      //// 非URL_basic页面,接受URL_basic页面返回的自定义集
      let origin = e.origin || e.originalEvent.origin;
      let t_collections, rev_index;
      if ((new RegExp(URL_basic).exec(origin))) {
        switch (e.data.COM) {
          case TCOM.UPDATE_COLLECTIONS:
            t_collections = e.data.data;
            rev_index = {};
            for (let i in customCollections) {
              rev_index[customCollections[i].name] = customCollections[i];
            }
            for (let i in t_collections) {
              if (rev_index.hasOwnProperty(t_collections[i].name)) {
                let target = rev_index[t_collections[i].name];
                target.contains.concat(t_collections[i].contains);
              } else {
                customCollections[i] = t_collections[i];
              }
            }
            saveCollection();
            break;
        }
      }
    }
    window.addEventListener("message", recvCollections, false);

    // 合并t.bilibili.com的自定义集记录
    iframe_t = document.createElement('iframe');
    iframe_t.style.display = 'none';
    document.body.appendChild(iframe_t);
    iframe_t.onload = () => {
      if (!!iframe_t.src.length) {
        iframe_t.contentWindow.postMessage({ COM: TCOM.FETCH_COLLECTIONS }, iframe_t.src);
      }
    };
    window.addEventListener("load", () => {
      iframe_t.src = `https://${URL_basic}/`;
    });
  }

  // main
  let users = [];
  let customCSSDOM = GM_addStyle(CSSSheet);
  setInterval(() => {
    let newUsersDOM = document.querySelectorAll(QS_NewUser);
    for (let i = 0; i < newUsersDOM.length; i++) {
      users.push(new TBilibiliUser(newUsersDOM[i]));
    }
  }, 5000);
})();