NGA Watcher

同步客户端关注功能

Versión del día 7/3/2021. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name        NGA Watcher
// @namespace   https://greasyfork.org/users/263018
// @version     1.1.0
// @author      snyssss
// @description 同步客户端关注功能

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

// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_addValueChangeListener
// @grant       GM_registerMenuCommand

// @noframes
// ==/UserScript==

((ui, self) => {
  if (!ui) return;

  // 钩子
  const hookFunction = (object, functionName, callback) => {
    ((originalFunction) => {
      object[functionName] = function () {
        const returnValue = originalFunction.apply(this, arguments);

        callback.apply(this, [returnValue, originalFunction, arguments]);

        return returnValue;
      };
    })(object[functionName]);
  };

  // STYLE
  GM_addStyle(`
    .s-user-info-container:not(:hover) .ah {
        display: none !important;
    }
    .s-table {
      border: 1px solid #ead5bc;
      border-left: none;
      border-bottom: none;
      width: 99.95%;
    }
    .s-table thead {
      background-color: #591804;
      color: #fff8e7;
    }
    .s-table tbody tr {
      background-color: #fff0cd;
    }
    .s-table tbody tr:nth-of-type(odd) {
      background-color: #fff8e7;
    }
    .s-table td {
      border: 1px solid #ead5bc;
      border-top: none;
      border-right: none;
      padding: 6px;
      white-space: nowrap;
    }
    .s-table input:not([type]) {
      margin: 0;
      width: 100%;
      box-sizing: border-box;
    }
  `);

  // 用户信息
  class UserInfo {
    execute(task) {
      task().finally(() => {
        if (this.waitingQueue.length) {
          const next = this.waitingQueue.shift();

          this.execute(next);
        } else {
          this.isRunning = false;
        }
      });
    }

    enqueue(task) {
      if (this.isRunning) {
        this.waitingQueue.push(task);
      } else {
        this.isRunning = true;

        this.execute(task);
      }
    }

    rearrange() {
      if (this.data) {
        const list = Object.values(this.children);

        for (let i = 0; i < list.length; i++) {
          if (list[i].source === undefined) {
            list[i].create(this.data);
          }

          Object.entries(this.container).forEach((item) => {
            list[i].clone(this.data, item);
          });
        }
      }
    }

    reload() {
      this.enqueue(async () => {
        this.data = await new Promise((resolve) => {
          fetch(`/nuke.php?lite=js&__lib=ucp&__act=get&uid=${this.uid}`)
            .then((res) => res.blob())
            .then((blob) => {
              const reader = new FileReader();

              reader.onload = () => {
                const text = reader.result;
                const result = JSON.parse(
                  text.replace("window.script_muti_get_var_store=", "")
                );

                resolve(result.data[0]);
              };

              reader.readAsText(blob, "GBK");
            })
            .catch(() => {
              resolve();
            });
        });

        Object.values(this.children).forEach((item) => item.destroy());

        this.rearrange();
      });
    }

    constructor(id) {
      this.uid = id;

      this.waitingQueue = [];
      this.isRunning = false;

      this.container = {};
      this.children = {};

      this.reload();
    }
  }

  // 用户信息组件
  class UserInfoWidget {
    destroy() {
      if (this.source) {
        this.source = undefined;
      }

      if (this.target) {
        Object.values(this.target).forEach((item) => {
          if (item.parentNode) {
            item.parentNode.removeChild(item);
          }
        });
      }
    }

    clone(data, [argid, container]) {
      if (this.source) {
        if (this.target[argid] === undefined) {
          this.target[argid] = this.source.cloneNode(true);

          if (this.callback) {
            this.callback(data, this.target[argid]);
          }
        }

        container.appendChild(this.target[argid]);
      }
    }

    constructor(func, callback) {
      this.create = (data) => {
        this.destroy();

        this.source = func(data);
        this.target = {};
      };

      this.callback = callback;
    }
  }

  ui.sn = ui.sn || {};
  ui.sn.userInfo = ui.sn.userInfo || {};

  ((info) => {
    // 关注
    const follow = (uid) =>
      new Promise((resolve, reject) => {
        fetch(
          `/nuke.php?lite=js&__lib=follow_v2&__act=follow&id=${uid}&type=1`,
          {
            method: "post",
          }
        )
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = JSON.parse(
                text.replace("window.script_muti_get_var_store=", "")
              );

              if (result.data) {
                resolve(result.data[0]);
              } else {
                reject(result.error[0]);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            resolve();
          });
      });

    // 取消关注
    const un_follow = (uid) =>
      new Promise((resolve, reject) => {
        fetch(
          `/nuke.php?lite=js&__lib=follow_v2&__act=follow&id=${uid}&type=8`,
          {
            method: "post",
          }
        )
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = JSON.parse(
                text.replace("window.script_muti_get_var_store=", "")
              );

              if (result.data) {
                resolve(result.data[0]);
              } else {
                reject(result.error[0]);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            resolve();
          });
      });

    // 移除粉丝
    const un_follow_fans = (uid) =>
      new Promise((resolve, reject) => {
        fetch(
          `/nuke.php?lite=js&__lib=follow_v2&__act=follow&id=${uid}&type=256`,
          {
            method: "post",
          }
        )
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = JSON.parse(
                text.replace("window.script_muti_get_var_store=", "")
              );

              if (result.data) {
                resolve(result.data[0]);
              } else {
                reject(result.error[0]);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            resolve();
          });
      });

    // 获取关注列表
    const follow_list = (page) =>
      new Promise((resolve, reject) => {
        fetch(
          `/nuke.php?lite=js&__lib=follow_v2&__act=get_follow&page=${page}`,
          {
            method: "post",
          }
        )
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = JSON.parse(
                text.replace("window.script_muti_get_var_store=", "")
              );

              if (result.data) {
                resolve(result.data[0]);
              } else {
                reject(result.error[0]);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            resolve();
          });
      });

    // 获取粉丝列表
    const follow_by_list = (page) =>
      new Promise((resolve, reject) => {
        fetch(
          `/nuke.php?lite=js&__lib=follow_v2&__act=get_follow_by&page=${page}`,
          {
            method: "post",
          }
        )
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = JSON.parse(
                text.replace("window.script_muti_get_var_store=", "")
              );

              if (result.data) {
                resolve(result.data[0]);
              } else {
                reject(result.error[0]);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            resolve();
          });
      });

    // 获取关注动态
    const follow_dymanic_list = () =>
      new Promise((resolve, reject) => {
        fetch(`/nuke.php?lite=js&__lib=follow_v2&__act=get_push_list`, {
          method: "post",
        })
          .then((res) => res.blob())
          .then((blob) => {
            const reader = new FileReader();

            reader.onload = () => {
              const text = reader.result;
              const result = JSON.parse(
                text.replace("window.script_muti_get_var_store=", "")
              );

              if (result.data) {
                resolve(result.data);
              } else {
                reject(result.error[0]);
              }
            };

            reader.readAsText(blob, "GBK");
          })
          .catch(() => {
            resolve();
          });
      });

    // UI
    const u = (() => {
      const modules = {};

      const createView = () => {
        const tabContainer = (() => {
          const c = document.createElement("div");

          c.className = "w100";
          c.innerHTML = `
            <div class="right_" style="margin-bottom: 5px;">
                <table class="stdbtn" cellspacing="0">
                    <tbody>
                        <tr></tr>
                    </tbody>
                </table>
            </div>
            <div class="clear"></div>
            `;

          return c;
        })();

        const tabPanelContainer = (() => {
          const c = document.createElement("div");

          c.style = "width: 40vw;";

          return c;
        })();

        const content = (() => {
          const c = document.createElement("div");

          c.append(tabContainer);
          c.append(tabPanelContainer);

          return c;
        })();

        const addModule = (() => {
          const tc = tabContainer.getElementsByTagName("tr")[0];
          const cc = tabPanelContainer;

          return (module) => {
            const tabBox = document.createElement("td");

            tabBox.innerHTML = `<a href="javascript:void(0)" class="nobr silver">${module.name}</a>`;

            const tab = tabBox.childNodes[0];

            const toggle = () => {
              Object.values(modules).forEach((item) => {
                if (item.tab === tab) {
                  item.tab.className = "nobr";
                  item.content.style = "display: block";
                  item.visible = true;
                } else {
                  item.tab.className = "nobr silver";
                  item.content.style = "display: none";
                  item.visible = false;
                }
              });

              module.refresh();
            };

            tc.append(tabBox);
            cc.append(module.content);

            tab.onclick = toggle;

            modules[module.name] = {
              ...module,
              tab,
              toggle,
              visible: false,
            };

            return modules[module.name];
          };
        })();

        return {
          content,
          modules,
          addModule,
        };
      };

      const refresh = () => {
        Object.values(modules)
          .find((item) => item.visible)
          ?.refresh();
      };

      return {
        createView,
        refresh,
      };
    })();

    // 我的关注
    {
      const name = "我的关注";

      const content = (() => {
        const c = document.createElement("div");

        c.style.display = "none";
        c.innerHTML = `
        <div style="max-height: 400px; overflow: auto;">
          <table class="s-table">
            <tbody></tbody>
          </table>
        </div>
        `;

        return c;
      })();

      let page = 0;
      let lastSize = -1;
      let isFetching = false;

      const box = content.querySelector("DIV");

      const list = content.querySelector("TBODY");

      const fetchData = () => {
        isFetching = true;

        follow_list(page)
          .then((res) => {
            lastSize = Object.keys(res).length;

            for (let i in res) {
              const { uid, username } = res[i];

              const name = `s-follow-${uid}`;

              if (list.querySelector(`#${name}`)) {
                continue;
              }

              const item = document.createElement("TR");

              item.id = name;
              item.innerHTML = `
              <td>
                <a href="/nuke.php?func=ucp&uid=${uid}" class="b nobr">[@${username}]</a>
              </td>
              <td width="1">
                <button>移除</button>
              </td>
            `;

              const action = item.querySelector("BUTTON");

              action.onclick = () => {
                if (confirm("取消关注?")) {
                  un_follow(uid).then(() => {
                    info[uid].reload();
                    u.refresh();
                  });
                }
              };

              list.appendChild(item);
            }
          })
          .finally(() => {
            isFetching = false;
          });
      };

      box.onscroll = () => {
        if (isFetching || lastSize === 0) {
          return;
        }

        if (box.scrollHeight - box.scrollTop - box.clientHeight <= 40) {
          page = page + 1;

          fetchData();
        }
      };

      const refresh = () => {
        list.innerHTML = "";

        page = 1;
        lastSize = -1;

        fetchData();
      };

      hookFunction(u, "createView", (view) => {
        view.addModule({
          name,
          content,
          refresh,
        });
      });
    }

    // 我的粉丝
    {
      const name = "我的粉丝";

      const content = (() => {
        const c = document.createElement("div");

        c.style.display = "none";
        c.innerHTML = `
          <div style="max-height: 400px; overflow: auto;">
            <table class="s-table">
              <tbody></tbody>
            </table>
          </div>
          `;

        return c;
      })();

      let page = 0;
      let lastSize = -1;
      let isFetching = false;

      const box = content.querySelector("DIV");

      const list = content.querySelector("TBODY");

      const fetchData = () => {
        isFetching = true;

        follow_by_list(page)
          .then((res) => {
            lastSize = Object.keys(res).length;

            for (let i in res) {
              const { uid, username } = res[i];

              const name = `s-fans-${uid}`;

              if (list.querySelector(`#${name}`)) {
                continue;
              }

              const item = document.createElement("TR");

              item.id = name;
              item.innerHTML = `
                <td>
                  <a href="/nuke.php?func=ucp&uid=${uid}" class="b nobr">[@${username}]</a>
                </td>
                <td width="1">
                  <button>移除</button>
                </td>
              `;

              const action = item.querySelector("BUTTON");

              action.onclick = () => {
                if (confirm("移除粉丝?")) {
                  un_follow_fans(uid).then(() => {
                    u.refresh();
                  });
                }
              };

              list.appendChild(item);
            }
          })
          .finally(() => {
            isFetching = false;
          });
      };

      box.onscroll = () => {
        if (isFetching || lastSize === 0) {
          return;
        }

        if (box.scrollHeight - box.scrollTop - box.clientHeight <= 40) {
          page = page + 1;

          fetchData();
        }
      };

      const refresh = () => {
        list.innerHTML = "";

        page = 1;
        lastSize = -1;

        fetchData();
      };

      hookFunction(u, "createView", (view) => {
        view.addModule({
          name,
          content,
          refresh,
        });
      });
    }

    // 关注动态
    {
      const name = "关注动态";

      const content = (() => {
        const c = document.createElement("div");

        c.style.display = "none";
        c.innerHTML = `
          <div style="max-height: 400px; overflow: auto;">
            <table class="s-table">
              <tbody></tbody>
            </table>
          </div>
          `;

        return c;
      })();

      let page = 0;
      let lastSize = -1;
      let isFetching = false;

      const box = content.querySelector("DIV");

      const list = content.querySelector("TBODY");

      const fetchData = () => {
        isFetching = true;

        follow_dymanic_list(page)
          .then((res) => {
            if (res[1] === res[2]) {
              lastSize = 0;
            } else {
              lastSize = -1;
            }

            return res[0];
          })
          .then((res) => {
            for (let i in res) {
              const id = res[i][0];
              const time = res[i][6];
              const summary = res[i]["summary"];

              const name = `s-follow-dymanic-${id}`;

              if (list.querySelector(`#${name}`)) {
                continue;
              }

              const parsedSummary = summary
                .replace(
                  /\[uid=(\d+)\](.+)\[\/uid\]/,
                  `<a href="/nuke.php?func=ucp&uid=$1" class="b nobr">$2</a>`
                )
                .replace(
                  /\[pid=(\d+)\](.+)\[\/pid\]/,
                  `<a href="/read.php?pid=$1" class="b nobr">回复</a>`
                )
                .replace(/\[tid=(\d+)\](.+)\[\/tid\]/, function ($0, $1, $2) {
                  let s = ui.cutstrbylen($2, 19);
                  if (s.length < $2.length) {
                    s += "...";
                  }

                  return `<a href="/read.php?tid=${$1}" class="b nobr">${s}</a>`;
                });

              const item = document.createElement("TR");

              item.id = name;
              item.innerHTML = `
                <td width="100">
                  ${ui.time2dis(time)}
                </td>
                <td>
                  ${parsedSummary}
                </td>
              `;

              list.appendChild(item);
            }
          })
          .finally(() => {
            isFetching = false;
          });
      };

      box.onscroll = () => {
        if (isFetching || lastSize === 0) {
          return;
        }

        if (box.scrollHeight - box.scrollTop - box.clientHeight <= 40) {
          page = page + 1;

          fetchData();
        }
      };

      const refresh = () => {
        list.innerHTML = "";

        page = 1;
        lastSize = -1;

        fetchData();
      };

      hookFunction(u, "createView", (view) => {
        view.addModule({
          name,
          content,
          refresh,
        });
      });
    }

    // 打开菜单
    const showMenu = (() => {
      let view, window;

      return () => {
        if (view === undefined) {
          view = u.createView();
        }

        view.modules["关注动态"].toggle();

        if (window === undefined) {
          window = ui.createCommmonWindow();
        }

        window._.addContent(null);
        window._.addTitle(`关注`);
        window._.addContent(view.content);
        window._.show();
      };
    })();

    // 增加菜单项
    if (document.querySelector(`[name="unisearchinput"]`)) {
      const anchor = document.querySelector("#mainmenu .td:last-child");

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

      button.className = `td`;
      button.innerHTML = `<a class="mmdefault" href="javascript: void(0);" style="white-space: nowrap;">关注</a>`;

      button.onclick = showMenu;

      anchor.before(button);
    }

    let popover;

    const execute = (argid) => {
      const args = ui.postArg.data[argid];

      if (args.comment) return;

      const uid = +args.pAid;

      if (uid > 0) {
        if (info[uid] === undefined) {
          info[uid] = new UserInfo(uid);
        }

        if (document.contains(info[uid].container[argid]) === false) {
          info[uid].container[argid] = args.uInfoC.querySelector(
            "[name=uid]"
          ).parentNode;
        }

        info[uid].enqueue(async () => {
          args.uInfoC.className =
            args.uInfoC.className + " s-user-info-container";

          if (info[uid].children[16]) {
            info[uid].children[16].destroy();
          }

          info[uid].children[16] = new UserInfoWidget(
            (data) => {
              const value = data.follow_by_num || 0;

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

              if (uid === self || data.follow) {
                element.className =
                  "small_colored_text_btn stxt block_txt_c2 vertmod";
              } else {
                element.className =
                  "small_colored_text_btn stxt block_txt_c2 vertmod ah";
              }

              element.style.cursor = "default";
              element.innerHTML = `<span class="white"><span style="font-family: comm_glyphs; -webkit-font-smoothing: antialiased; line-height: 1em;">★</span>&nbsp;${value}</span>`;

              element.style.cursor = "pointer";

              return element;
            },
            (data, element) => {
              if (!self) return;

              const handleClose = () => {
                if (popover) {
                  popover.style.display = "none";
                }
              };

              const handleSwitchFollow = () => {
                if (data.follow) {
                  if (confirm("取消关注?")) {
                    un_follow(data.uid).then(() => {
                      info[uid].reload();
                      u.refresh();
                    });
                  }
                } else {
                  follow(data.uid).then(() => {
                    info[uid].reload();
                    u.refresh();
                  });
                }

                handleClose();
              };

              element.onclick = (e) => {
                if (uid === self) {
                  showMenu();
                  return;
                }

                if (!popover) {
                  popover = document.createElement("SPAN");

                  popover.className = "urltip2 urltip3 ah";
                  popover.style = "textAlign: left; margin: 0;";
                }

                if (element.parentNode !== popover.parentNode) {
                  element.parentNode.appendChild(popover);
                }

                if (data.follow) {
                  if (popover.type !== 1) {
                    popover.type = 1;
                    popover.innerHTML = `<nobr>
                      <a href="javascript: void(0);">[已关注]</a>
                      <a href="javascript: void(0);">[关闭]</a>
                    </nobr>`;

                    const buttons = popover.getElementsByTagName("A");

                    buttons[0].onclick = handleSwitchFollow;
                    buttons[1].onclick = handleClose;
                  }
                } else {
                  if (popover.type !== 2) {
                    popover.type = 2;
                    popover.innerHTML = `<nobr>
                        <a href="javascript: void(0);">[关注]</a>
                        <a href="javascript: void(0);">[关闭]</a>
                    </nobr>`;

                    const buttons = popover.getElementsByTagName("A");

                    buttons[0].onclick = handleSwitchFollow;
                    buttons[1].onclick = handleClose;
                  }
                }

                popover.style.left = `${e.pageX}px`;
                popover.style.top = `${e.pageY}px`;
                popover.style.display = "block";
              };
            }
          );

          info[uid].rearrange();
        });
      }
    };

    if (ui.postArg) {
      Object.keys(ui.postArg.data).forEach((i) => execute(i));
    }

    let initialized = false;

    hookFunction(ui, "eval", () => {
      if (initialized) return;

      if (ui.postDisp) {
        hookFunction(
          ui,
          "postDisp",
          (returnValue, originalFunction, arguments) => execute(arguments[0])
        );

        initialized = true;
      }
    });
  })(ui.sn.userInfo);
})(commonui, __CURRENT_UID);