NGA Watcher

同步客户端关注功能

目前為 2021-03-07 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);