NGA Watcher

同步客户端关注功能

Versione datata 07/03/2021. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name        NGA Watcher
// @namespace   https://greatest.deepsurf.us/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);