// ==UserScript==
// @name 抒发森林增强
// @namespace https://github.com/rktccn/treehollowEnhance
// @supportURL https://github.com/rktccn/treehollowEnhance
// @homepageURL https://github.com/rktccn/treehollowEnhance
// @version 0.3.1
// @description 抒发森林增强,只看洞主,下载图片
// @author RoIce
// @match *://web.treehollow.net/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=greasespot.net
// @grant GM_addStyle
// @grant GM_notification
// @run-at document-start
// @license MIT
// ==/UserScript==
// 2022-4-28 更新屏蔽功能和菜单开关功能
// 修复回到顶部的问题,应该修复了
(function () {
"use strict";
// Your code here...
let css = `.active{
background-color: rgb(243, 142, 4) !important;
}`;
GM_addStyle(css);
// 原始数据
let originalreplyList = [];
// 通知信息
let noticeLength = 0; // 总通知数
let newNoticeList = []; // 新通知
let noticeList = []; // 临时储存通知
// 回复内容节点
let replyNodes = [];
// 检测屏幕大小,是否为移动端
const isMobile = () => {
return window.screen.width < 768;
};
// 拦截获取请求
function addXMLRequestCallback(callback) {
var oldSend, i;
if (XMLHttpRequest.callbacks) {
// we've already overridden send() so just add the callback
XMLHttpRequest.callbacks.push(callback);
} else {
// create a callback queue
XMLHttpRequest.callbacks = [callback];
// store the native send()
oldSend = XMLHttpRequest.prototype.send;
// override the native send()
XMLHttpRequest.prototype.send = function () {
// process the callback queue
// the xhr instance is passed into each callback but seems pretty useless
// you can't tell what its destination is or call abort() without an error
// so only really good for logging that a request has happened
// I could be wrong, I hope so...
// EDIT: I suppose you could override the onreadystatechange handler though
for (i = 0; i < XMLHttpRequest.callbacks.length; i++) {
XMLHttpRequest.callbacks[i](this);
}
// call the native send()
oldSend.apply(this, arguments);
};
}
}
// 监听通知请求
const listenNotification = () => {
addXMLRequestCallback(function (xhr) {
xhr.addEventListener("load", function () {
if (xhr.readyState == 4 && xhr.status == 200) {
if (xhr.responseURL.includes("user/notifications")) {
//do something!
// 去重,将新通知添加到通知列表
let $notice = JSON.parse(xhr.response);
newNoticeList = $notice.slice(0, $notice.length - noticeLength);
noticeLength = $notice.length;
if (newNoticeList.length !== 0) {
newNoticeList.forEach((item) => {
if (item.type === "replyPost") {
noticeList.push({
pid: item.pid,
userName: item.name,
content: item.content,
});
}
});
}
}
}
});
});
};
// 获取回复原数据
function getOriginalData() {
addXMLRequestCallback(function (xhr) {
xhr.addEventListener("load", function () {
if (xhr.readyState == 4 && xhr.status == 200) {
if (xhr.responseURL.includes("holes/detail")) {
//do something!
originalreplyList = JSON.parse(xhr.response).replies;
if (!originalreplyList?.length) {
originalreplyList = [];
throw new Error("获取源数据失败或没有回复");
}
}
}
});
});
}
// 获取一段文字中所有大写字母
function getUpperCase(str) {
let arr = [];
for (let i = 0; i < str.length; i++) {
if (str[i] === str[i].toUpperCase() && str[i] !== " ") {
arr.push(str[i]);
}
}
return arr.join("");
}
// 检测是否加载完成,异步调用
const checkLoad = () =>
new Promise((resolve, reject) => {
let check = setInterval(() => {
let targetNode = document.querySelector(
"#root > div > div > div > div > div > div > div > div > div.css-1dbjc4n.r-13awgt0 > div > div.css-1dbjc4n.r-1p0dtai.r-1d2f490.r-12vffkv.r-u8s1d.r-zchlnj.r-ipm5af > div.css-1dbjc4n.r-13awgt0.r-12vffkv > div > div > div > div > div > div.css-1dbjc4n.r-1kihuf0.r-e7q0ms"
);
if (targetNode !== null) {
clearInterval(check);
resolve(true);
}
}, 200);
});
// 获取回复
const getReply = () => {
const targetNode = document.getElementsByClassName(
"css-1dbjc4n r-1kihuf0 r-knv0ih r-e7q0ms"
);
if (!targetNode) {
throw new Error("获取回复失败");
}
hideUser.hideReplay();
return targetNode;
};
// 监听楼层数量的变化
const listenReplayCount = () => {
let targetNode = document.querySelector(
"#root > div > div > div > div > div > div > div > div > div.css-1dbjc4n.r-13awgt0 > div > div.css-1dbjc4n.r-1p0dtai.r-1d2f490.r-12vffkv.r-u8s1d.r-zchlnj.r-ipm5af > div.css-1dbjc4n.r-13awgt0.r-12vffkv > div > div > div > div > div > div.css-1dbjc4n.r-1kihuf0.r-13awgt0.r-knv0ih.r-13qz1uu > div > div > div:nth-child(2)"
);
const config = { childList: true };
const callBack = (mutationsList, observer) => {
replyNodes = getReply();
};
const observer = new MutationObserver(callBack);
observer.observe(targetNode, config);
};
// 点击只看洞主
const clickDZ = (e) => {
if (originalreplyList?.length === 0) return;
if (hideUser.onlySeeUserName === "") {
e.target.classList.add("active");
e.target.innerHTML = `只看洞主`;
hideUser.onlySee("洞主");
} else {
e.target.classList.remove("active");
e.target.innerHTML = `只看${hideUser.onlySeeUserName}`;
hideUser.onlySee("");
}
hideUser.hideReplay();
};
// 查看所有图片
const getImg = () => {
let imgList = [];
// 提取图片
originalreplyList.forEach((item) => {
if (item?.image) {
imgList.push(`https://img.treehollow.net/${item.image.src}`);
}
});
return imgList;
};
// 新增图片显示窗口
const addImgWindow = () => {
let container = document.createElement("div");
container.classList = "img-container";
container.style.cssText =
"position: fixed; inset: 0px; z-index: 9999; background-color: rgba(0, 0, 0, 0.6); overflow-y: scroll; display: grid; grid-template-columns: 1fr 1fr 1fr; width: 100vw; height: 100vh;gap: 16px;";
document.body.appendChild(container);
let imgList = getImg();
for (let i = 0; i < imgList.length; i++) {
const imgUrl = imgList[i];
let img = document.createElement("img");
img.src = imgUrl;
img.style.cssText = "width: 100%;";
container.appendChild(img);
}
let close = document.createElement("div");
close.innerText = "关闭";
close.style.cssText =
" position: fixed; right: 20px; bottom: 50%; margin-bottom: 16px; background-color: #53A13C; border-radius: 5px; cursor: pointer; display: inline-block; font-size: 17px; font-weight: 400;;;; width: 50px;line-height: 1.2; padding: 17px 14px; text-align: center; text-decoration: none; color: #fff;";
close.addEventListener("click", () => {
container.remove();
});
container.appendChild(close);
};
// 回到顶部
const goTop = () => {
let targetNode = document.getElementsByClassName(
"css-1dbjc4n r-1kihuf0 r-1x0uki6 r-e7q0ms"
);
targetNode[0].scrollIntoView({ behavior: "smooth" });
};
// 格式化回复
const formatReply = (replyNode) => {
let content =
replyNode
.getElementsByClassName(
"css-901oao r-jwli3a r-ubezar r-13uqrnb r-16dba41 r-oxtfae r-dhbnww r-1xnzce8"
)[0]
?.innerText.trim() || "";
let userName = replyNode
.getElementsByClassName(
"css-901oao r-5rif8m r-ubezar r-13uqrnb r-majxgm r-oxtfae r-dhbnww r-13hce6t r-14gqq1x"
)[0]
?.innerText.trim();
let reply =
replyNode
.getElementsByClassName(
"css-901oao css-vcwn7f r-5rif8m r-1b43r93 r-13uqrnb r-16dba41 r-oxtfae r-dhbnww r-1jkjb r-icoktb"
)[0]
?.innerText.trim() || "";
if (!userName) {
throw new Error("格式化回复信息失败");
}
return {
content,
userName,
reply,
};
};
// 隐藏指定用户按钮
const hideUser = {
className: "hide-user",
userList: [], // 隐藏用户名列表
contentList: [], // 隐藏文本列表
onlySeeUserName: "", // 只看指定用户
isShow: false, // 是否显示列表界面
// param {string} val 文本内容
// param {string} type 类型 content/user
addToHideList: (val, type) => {
type === "content"
? hideUser.contentList.push(val.trim())
: hideUser.userList.push(val.trim());
},
removeFromHideList: (val, type) => {
type === "content"
? hideUser.contentList.splice(
hideUser.contentList.indexOf(val.trim()),
1
)
: hideUser.userList.splice(hideUser.userList.indexOf(val.trim()), 1);
},
// 新建隐藏列表窗口
addWindow: () => {
let container = document.createElement("div");
container.className = "hide-container";
container.style.cssText =
"position: absolute; z-index: 9999; background-color: rgba(0, 0, 0, 0.6); overflow-y: scroll; width: 260px; max-height: 230px; gap: 16px; padding: 16px;right: 120px;top: 30%; color: #fff;";
document.body.appendChild(container);
hideUser.addUserListDOM();
hideUser.addContentListDOM();
},
// 移除隐藏列表窗口
removeWindow: () => {
let container = document.getElementsByClassName("hide-container")[0];
if (container !== undefined) {
container.remove();
}
},
// 新增隐藏用户列表
addUserListDOM: () => {
let container = document.getElementsByClassName("hide-container")[0];
if (container === undefined) {
hideUser.addWindow();
container = document.getElementsByClassName("hide-container")[0];
}
let userList = document.createElement("div");
userList.style.cssText =
"display: flex; flex-direction: column;align-items: flex-start; color: #fff; fontsize: 24px; font-weight: 600; margin-bottom: 16px;";
userList.innerHTML = "被隐藏的用户(当前贴有效)";
for (let i = 0; i < hideUser.userList.length; i++) {
const userName = hideUser.userList[i];
let element = document.createElement("div");
element.innerText = `${userName} --- 点击移除`;
element.style.cssText =
"display: inline-block; font-size: 16px; font-weight: 400; margin-top: 8px; cursor: pointer;";
element.addEventListener("click", () => {
hideUser.removeFromHideList(userName, "user");
element.remove();
hideUser.hideReplay();
});
userList.appendChild(element);
}
// 输入框
let input = document.createElement("input");
input.style.cssText =
"width: 100%; height: 40px; border-radius: 4px; border: 1px solid #fff; margin-bottom: 16px;";
input.placeholder = "输入用户名首字母 如:AD";
input.addEventListener("keyup", (e) => {
if (e.keyCode === 13) {
let name = input.value.trim();
hideUser.addToHideList(name, "user");
// 添加用户名到列表
let element = document.createElement("div");
element.innerText = `${name} --- 点击移除`;
element.style.cssText =
"display: inline-block; font-size: 16px; font-weight: 400; margin-top: 8px; cursor: pointer;";
element.addEventListener("click", () => {
hideUser.removeFromHideList(name, "user");
element.remove();
hideUser.hideReplay();
});
userList.insertBefore(element, input);
hideUser.hideReplay();
input.value = "";
}
});
userList.appendChild(input);
container.appendChild(userList);
},
// 新增隐藏文本列表
addContentListDOM: () => {
let container = document.getElementsByClassName("hide-container")[0];
if (container === undefined) {
hideUser.addWindow();
container = document.getElementsByClassName("hide-container")[0];
}
let contentList = document.createElement("div");
contentList.style.cssText =
"display: flex; flex-direction: column;align-items: flex-start; color: #fff; fontsize: 24px; font-weight: 600; margin-bottom: 16px;";
contentList.innerHTML = "被隐藏的文本";
for (let i = 0; i < hideUser.contentList.length; i++) {
const content = hideUser.contentList[i];
let element = document.createElement("div");
element.innerText = `${content} --- 点击移除`;
element.style.cssText =
"display: inline-block; font-size: 16px; font-weight: 400; margin-top: 8px; cursor: pointer;";
element.addEventListener("click", () => {
hideUser.removeFromHideList(content, "content");
element.remove();
hideUser.hideReplay();
});
contentList.appendChild(element);
}
// 输入框
let input = document.createElement("input");
input.style.cssText =
"width: 100%; height: 40px; border-radius: 4px; border: 1px solid #fff; margin-bottom: 16px;";
input.placeholder = "输入文本 如:cy";
input.addEventListener("keyup", (e) => {
if (e.keyCode === 13) {
let content = input.value.trim();
hideUser.addToHideList(content, "content");
// 添加文本到列表
let element = document.createElement("div");
element.innerText = `${content} --- 点击移除`;
element.style.cssText =
"display: inline-block; font-size: 16px; font-weight: 400; margin-top: 8px; cursor: pointer;";
element.addEventListener("click", () => {
hideUser.removeFromHideList(content, "content");
element.remove();
hideUser.hideReplay();
});
contentList.insertBefore(element, input);
hideUser.hideReplay();
input.value = "";
}
});
contentList.appendChild(input);
container.appendChild(contentList);
},
// 执行只看某人
onlySee: (val = "洞主") => {
if (hideUser.onlySeeUserName === "") {
hideUser.onlySeeUserName = val.trim();
} else {
hideUser.onlySeeUserName = "";
}
},
// 检查是否需要隐藏
checkHide: (replyNode) => {
if (originalreplyList?.length === 0) return false;
let { content, userName } = formatReply(replyNode);
if (hideUser.onlySeeUserName !== "") {
// 只看指定用户
if (userName !== hideUser.onlySeeUserName) {
return true;
}
} else {
if (
hideUser.userList.includes(getUpperCase(userName)) ||
hideUser.contentList.includes(content)
) {
return true;
}
}
return false;
},
// 隐藏内容
hideReplay: () => {
for (let i = 0; i < replyNodes.length; i++) {
if (hideUser.checkHide(replyNodes[i])) {
replyNodes[i].style.display = "none";
} else {
replyNodes[i].style.display = "block";
}
}
},
clickHandler: (e) => {
hideUser.isShow = !hideUser.isShow;
hideUser.isShow ? hideUser.addWindow() : hideUser.removeWindow();
hideUser.isShow
? e.target.classList.add("active")
: e.target.classList.remove("active");
},
};
// 显示/隐藏button-container
const showMenu = {
isShow: false,
show: (e) => {
showMenu.isShow = true;
let container = document.getElementsByClassName("button-container")[0];
for (let i = 0; i < container.childNodes.length - 1; i++) {
const element = container.childNodes[i];
element.style.transform = "translateX(0)";
}
if (e) {
e.target.innerText = "关闭菜单";
}
},
hide: (e) => {
showMenu.isShow = false;
let container = document.getElementsByClassName("button-container")[0];
for (let i = 0; i < container.childNodes.length - 1; i++) {
const element = container.childNodes[i];
element.style.transform = "translateX(100%)";
element.style.transition = "all 0.3s ease-in-out";
}
if (e) {
e.target.innerText = "显示菜单";
}
},
clickHandler: (e) => {
if (showMenu.isShow) {
showMenu.hide(e);
showMenu.isShow = false;
} else {
showMenu.show(e);
showMenu.isShow = true;
}
},
};
// button 相关
const button = {
// 添加按钮
addButton: (text, callback = () => {}, className = "") => {
let pcCss = {
container:
"display: flex; justify-content: center; align-items: center; position: fixed; right: 20px; bottom: 50%;flex-direction: column;transform: translateY(55%);",
button:
"z-index: 9999; margin-bottom: 16px; background-color: #53A13C; border-radius: 5px; cursor: pointer; display: inline-block; font-size: 17px; font-weight: 400;;;; width: 50px;line-height: 1.2; padding: 17px 14px; text-align: center; text-decoration: none; color: #fff;",
};
let mobileCss = {
container:
"display: flex; justify-content: center; align-items: center; position: fixed; right: 10px; bottom: 30%; flex-direction: column; transform: translateY(30%);",
button:
"z-index: 9999; margin-bottom: 16px; background-color: rgb(83, 161, 60); border-radius: 5px; cursor: pointer; display: inline-block; font-weight: 400; width: 35px; line-height: 1.2; padding: 10px 9px; text-align: center; text-decoration: none; color: rgb(255, 255, 255);font-size: 12px;",
};
let container = document.getElementsByClassName("button-container")[0];
if (container === undefined) {
// 添加container
container = document.createElement("div");
container.className = `button-container ${className}`;
container.style.cssText = isMobile()
? mobileCss.container
: pcCss.container;
document.body.appendChild(container);
}
const element = document.createElement("div");
element.innerText = text;
element.className = "dz-button";
element.style.cssText = isMobile() ? mobileCss.button : pcCss.button;
element.addEventListener("click", callback);
container.appendChild(element);
},
// 移除按钮
removeButton: () => {
let container = document.getElementsByClassName("button-container")[0];
if (container !== undefined) {
container.remove();
}
},
};
// 新建button
const addButton = (name, clickCallBack, className = "") => {
button.addButton(name, clickCallBack, className);
};
/**
* 重写history的pushState和replaceState
* @param action pushState|replaceState
* @return {function(): *}
*/
function wrapState(action) {
// 获取原始定义
let raw = history[action];
return function () {
// 经过包装的pushState或replaceState
let wrapper = raw.apply(this, arguments);
// 定义名为action的事件
let e = new Event(action);
// 将调用pushState或replaceState时的参数作为stateInfo属性放到事件参数event上
e.stateInfo = { ...arguments };
// 调用pushState或replaceState时触发该事件
window.dispatchEvent(e);
return wrapper;
};
}
//修改原始定义
history.pushState = wrapState("pushState");
history.replaceState = wrapState("replaceState");
// 初始化数据
const initData = () => {
button.removeButton();
hideUser.removeWindow();
hideUser.onlySeeUserName = "";
if (window.location.pathname == "/HoleDetail") {
getOriginalData();
checkLoad().then((res) => {
replyNodes = getReply();
listenReplayCount();
button.removeButton();
addButton("只看洞主", clickDZ);
addButton("下载图片", addImgWindow);
addButton("回到顶部", goTop);
addButton("隐藏用户", hideUser.clickHandler);
addButton("显示菜单", showMenu.clickHandler);
showMenu.hide();
});
}
if (
window.location.pathname !== "/HoleDetail" &&
window.location.pathname !== "/ReplyModal"
) {
hideUser.userList = [];
}
};
// 监听自定义的事件
window.addEventListener("pushState", function (e) {
initData();
});
window.addEventListener("replaceState", function (e) {
initData();
});
})();