NGA Watcher

同步客户端关注功能

目前为 2021-03-07 提交的版本。查看 最新版本

// ==UserScript==
// @name        NGA Watcher
// @namespace   https://gf.qytechs.cn/users/263018
// @version     1.1.1
// @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);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址