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