// ==UserScript==
// @name 手机百度贴吧自动展开楼层
// @namespace http://tampermonkey.net/
// @homepage https://greasyfork.org/scripts/445657
// @version 2.1
// @description 有时候用手机的浏览器打开百度贴吧,只想看一眼就走,并不想打开APP,这个脚本用于帮助用户自动展开楼层。注意:只支持手机浏览器,测试环境为Iceraven+Tampermonkey
// @author voeoc
// @match https://tieba.baidu.com/p/*
// @match https://jump2.bdimg.com/p/*
// @match https://tiebac.baidu.com/p/*
// @connect https://tieba.baidu.com/mg/o/getFloorData
// @connect https://jump2.bdimg.com/mg/o/getFloorData
// @connect https://tiebac.baidu.com/mg/o/getFloorData
// @icon https://tieba.baidu.com/favicon.ico
// @grant unsafeWindow
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @license MIT
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
const DEBUGLOG_SWITCH = false; // 调试信息开关
const isFixedTitle = false; // 是否将标题置顶
const isAutoExpand = true; // 自动展开的开关
const eachExpandSize = 15; // 每次展开的评论数量,至少为10,少于10按10计算
const remaindAutoExpandSize = 15; // 当剩余评论少于这个数时,执行自动展开
const STR_NEWOPENLZLTEXT = "展开评论"; // 打开楼中楼按钮的文本
const STR_REMAINDOPENLZLTEXT = function(num){
return `剩余${num}个评论`;
}
const STR_VOEOCMARK = "VOEOCMARK"; // 临时标记
const REG_URL_POSTPAGE = RegExp(`postPage\?(?=.*tid\=)(?=.*postAuthorId\=)(?=.*forumId\=)`, 'i'); // 评论页url正则
const REG_URL_FLOORREPLAYPAGE = RegExp(`lzlPage\?(?=.*floor\=)(?=.*pid\=)`, 'i'); // 楼中楼页url正则
const REG_URL_JSON_FLOORDATA = RegExp(`getFloorData\?(?=.*pn\=)(?=.*rn\=)(?=.*tid\=)(?=.*pid\=)`, 'i'); // 楼中楼json数据
const REG_URL_POSTPBDATA = RegExp(`getPbData\?(?=.*pn\=)(?=.*rn\=)(?=.*only_post\=)(?=.*kz\=)`, 'i'); // 评论页数据url正则
const REG_URL_MAINPBDATA = RegExp(`getPbData\?(?=.*eqid\=)(?=.*refer\=)(?=.*pn\=)(?=.*rn\=)(?=.*format\=)(?=.*obj_param2\=)(?=.*kz\=)`, 'i'); // 主页数据url正则
const REG_URL_PBDATA = RegExp(`getPbData\?(?=.*pn\=)(?=.*rn\=)(?=.*kz\=)`, 'i'); // 通用页面数据url正则
const PAGE_TYPE = { // 页面类型
UNKNOW: -1, // 未知
MAINPAGE: 0, // 主页
POSTPAGE: 1, // 评论页
FLOORREPLAYPAGE: 2, // 楼中楼页
};
let tie = undefined; // 一楼
let tiebaName = undefined; // 吧名
let lzId = ""; // 楼主id
let currentHash = unsafeWindow.location.hash; // 存储当前页的Hash
let oldHash = currentHash; // 存储上一链接的Hash
let scrollPos = undefined; // 存储当前滚动位置
let floorDataList = {} // 存储所有楼层数据以便搜索,索引为楼层数字符串,值为抓取的json。主要信息为pid,获取路径floorDataList[floor].id
let currentUrlInfo = { // 截取当前url
host: "tieba.baidu.com",
tid: "",
postAuthorId: "",
forumId: "",
}
function DEBUGLOG(msg, label = "") {
if (!DEBUGLOG_SWITCH) {
return;
}
let outputFunc = console.log;
if (label == "error") {
outputFunc = console.error;
}
outputFunc(`voeoc(DEBUG)<${label}>: ${msg}`);
}
function waitElementLoaded(selector, func, TIME_OUT = 30) {
//const TIME_OUT = 30; // 找n次没有找到就放弃
let findTimeNum = 0; // 记录查找的次数
let timer = setInterval(() => {
let element = document.querySelector(selector);
DEBUGLOG(`${selector}=${element}`, "waitElementLoaded");
if (element != null) {
// 清除定时器
clearInterval(timer);
func(element);
} else {
findTimeNum++;
if (TIME_OUT < findTimeNum) {
// 清除定时器
clearInterval(timer);
}
}
}, 200);
}
// 获取url参数
function getUrlAttr(url, attrName) {
return RegExp(`${attrName}=([^&]*)&?`, 'i').exec(url)[1].trim();
}
// 简单判断当前页面的类型
function getPageType(hash = unsafeWindow.location.hash) {
if (hash === "" || hash === "#/") {
return PAGE_TYPE.MAINPAGE;
} else if (REG_URL_POSTPAGE.test(hash)) {
return PAGE_TYPE.POSTPAGE;
} else if (REG_URL_FLOORREPLAYPAGE.test(hash)) {
return PAGE_TYPE.FLOORREPLAYPAGE;
}
return PAGE_TYPE.UNKNOW;
}
// 展开对应楼中楼
function openLzlPage(floorNum, floorElement, expandBtn) {
// 另一种打开楼中楼的方法,但是需要页面切换
//let newHash = `#/lzlPage?tid=${currentUrlInfo.tid}&pid=${pid}&floor=${floorNum}&postAuthorId=${currentUrlInfo.postAuthorId}&forumId=${currentUrlInfo.forumId}`;
//DEBUGLOG(newHash, "newHash");
//unsafeWindow.location.hash = newHash;
let pid = floorDataList[floorNum].id; // 楼层id
let current_page = parseInt(expandBtn.getAttribute("current_page")); // 当前页数
let page_size = eachExpandSize; // 单个页面评论数量,至少为10
if(page_size < 10) {
page_size = 10;
}
// 按钮动画
expandBtn.disabled = true;
expandBtn.classList.add("loading");
function recoverExpandBtn() {
expandBtn.disabled = false;
expandBtn.classList.remove("loading");
}
let url = `${unsafeWindow.origin}/mg/o/getFloorData?pn=${current_page}&rn=${page_size}&tid=${currentUrlInfo.tid}&pid=${pid}`;
DEBUGLOG(url, "GM_xmlhttpRequest");
// 使用GM_xmlhttpRequest前需要添加对应url的@connect标记
GM_xmlhttpRequest({
method: "get",
url: url,
onload: function(details){
try {
// 爬取解析楼中楼评论数据
let floorData = JSON.parse(details.responseText);
let pageinfo = floorData.data.page; // 楼中楼信息,包括楼层数、页面大小、页面数量
let pageSelector = undefined;
let subpostlist = floorData.data.sub_post_list; // 评论列表
// 加载楼中楼
let lzlParent = floorElement.querySelector("div.lzl-post");
let originItemList = lzlParent.getElementsByClassName("lzl-post-item");
let sampleItem = originItemList[0].cloneNode(true);
// 获取dataset数据
let dvlist = []
for(let dv in originItemList[0].querySelector(".thread-text").dataset){
dvlist.push(`data-${dv}`);
}
const CONTENT_TYPE = { // 页面类型
TEXT: 0, // 文本
EMOJI: 2, // 表情
USERNAME: 4, // 用户名,一般用作回复
};
//lzlParent.innerHTML = ""; // 删除原有
// 去掉前两个评论
if(current_page == 1) {
subpostlist.shift();
subpostlist.shift();
}
subpostlist.forEach(function(subpost){
let allContent = "";
let userid = subpost.author.id;
subpost.content.forEach(function(content){
let item = "";
switch(content.type) {
case CONTENT_TYPE.EMOJI:
item = `
`
break;
case CONTENT_TYPE.USERNAME:
item = ` ${content.text} `
break;
case CONTENT_TYPE.TEXT:
default:
// 如有其他的类型暂时用文本代替
item = `${content.text}`;
break;
}
allContent += item;
})
let newItem = sampleItem.cloneNode(true);
newItem.querySelector(".username").innerHTML = `${subpost.author.show_nickname} ${lzId == subpost.author.id ?
``: ""}:`;
newItem.querySelector(".thread-text").innerHTML = allContent;
lzlParent.insertBefore(newItem, expandBtn.parentNode);
});
let total_page = parseInt(pageinfo.total_page);
if(total_page > current_page) { // 仍有剩余评论未展开
expandBtn.setAttribute("current_page", current_page + 1);
let total_num = parseInt(pageinfo.total_num);
let remaind_num = total_num - page_size*current_page;
expandBtn.innerHTML = STR_REMAINDOPENLZLTEXT(remaind_num);
if(remaindAutoExpandSize > remaind_num) {
expandBtn.onclick();
}
} else { // 所有评论已展开
expandBtn.parentNode.style.display = "none";
expandBtn.style.display = "none";
}
} finally {
recoverExpandBtn();
}
},
onerror: function(details) {
recoverExpandBtn();
console.error(`无法加载评论区,爬取的url为${url}`);
},
onabort: onerror,
ontimeout: onerror,
});
}
function customizeExpandBtn(floorParent) {
// 监听页面改变事件的回调
function onNewFloorAdded(newFloor){
if(newFloor.classList.contains(STR_VOEOCMARK)) { // 已被打上标记
return;
}
newFloor.classList.add(STR_VOEOCMARK);
let expandBtnParent = newFloor.querySelector(".open-app-guide"); // 楼中楼展开按钮
let lzlParent = newFloor.querySelector("div.lzl-post");
if(!expandBtnParent) { // 原来的展开楼中楼在App查看按钮不存在
return;
}
let floorinfo = newFloor.querySelector(".floor-info"); // 楼层数
if(!floorinfo) { // 楼层数获取失败
return;
}
// 爬取当前的楼层数
let floorNum = RegExp(`第([0-9]+)楼`, 'i').exec(floorinfo.innerHTML)[1].trim();
// 创建新按钮
let expandBtn = document.createElement( "span");
expandBtn.className = "open-app-text-real";
expandBtn.innerHTML = STR_NEWOPENLZLTEXT;
expandBtn.setAttribute("current_page", 1);
expandBtn.onclick = function() {
openLzlPage(floorNum, newFloor, expandBtn);
};
// 绑定点击事件,点击评论区时可以触发展开
expandBtnParent.onclick = function() {
if(expandBtn.style.display !== "none") {
expandBtn.onclick();
}
};
// 替换新按钮
expandBtnParent.insertBefore(expandBtn, expandBtnParent.children[0]);
if(isAutoExpand) {
expandBtn.onclick();
}
};
// 使用新按钮刷新楼层
function searchAndUpdatePostPage() {
// 遍历所有楼层元素
let dlist = floorParent.querySelectorAll(`div.post-item:not(.${STR_VOEOCMARK})`);
dlist.forEach(onNewFloorAdded);
}
// 注册楼层元素添加事件
let config = {
attributes: false,
childList: true,
characterData: false,
subtree: false,
};
let observer = new MutationObserver(function(mutationList) {
searchAndUpdatePostPage();
});
observer.observe(floorParent, config);
searchAndUpdatePostPage();
}
// 检测URL Hash变化,当force为true时,无论是否变化均执行后续任务
function checkUrlHashChange(force = false) {
let newHash = unsafeWindow.location.hash;
if(currentHash != newHash) {
currentHash = newHash;
} else {
if(!force) {
return false;
}
}
let pageType = getPageType();
if (pageType == PAGE_TYPE.POSTPAGE) {
// 收集url数据
currentUrlInfo = {
host: unsafeWindow.location.hostname,
tid: getUrlAttr(currentHash, "tid"),
postAuthorId: getUrlAttr(currentHash, "postAuthorId"),
forumId: getUrlAttr(currentHash, "forumId"),
}
// 当页面变动时,刷新展开楼层的按钮
waitElementLoaded(".post-page-list", (postpagelist) => { // 等待页面加载完成
customizeExpandBtn(postpagelist);
}, 10);
// 恢复一楼显示
restore();
// 页面切换后恢复滚动位置
scrollTo(scrollPos);
} else if(pageType == PAGE_TYPE.MAINPAGE) {
if(tie) {
// 当页面变动时,刷新展开楼层的按钮
waitElementLoaded(".pb-page-wrapper", (pbpage) => { // 等待页面加载完成
customizeExpandBtn(pbpage);
}, 10);
// 将剪切走的一楼复制回来
waitElementLoaded("#replySwitch", (splitline) => { // 等待页面加载完成
splitline.parentNode.insertBefore(tie, splitline);
}, 10);
scrollTo(0);
}
}
return true;
}
// 滚动到指定y坐标(如果当前楼层数比较大,只能滚动到贴末尾的最大加载位置)
function scrollTo(yPos) {
waitElementLoaded(".post-page", (postpage) => { // 等待页面加载完成
document.documentElement.scrollTop = yPos;
// 在一定时间内维持滚动位置
setTimeout(function () {
document.documentElement.scrollTop = yPos;
DEBUGLOG(yPos, "scrollTo");
}, 200);
});
}
// 显示一楼的内容
function restore() {
DEBUGLOG("restore")
waitElementLoaded(".text", (titletext) => { // 等待标题位置加载
// 显示贴吧名
try {
let tiebaNameclone = tiebaName.cloneNode(true);
titletext.parentNode.replaceChild(tiebaNameclone, titletext);
// 关联点击贴吧名的事件
tiebaNameclone.onclick = function () {
tiebaName.click();
}
} catch (e) { console.error(e); }
// 显示楼主发帖层
try {
// 复原样式丢失
tie.style.cssText = `
margin-left: 0.12rem;
margin-right: 0.12rem;
margin-bottom: 0.25rem;
`
// 尝试找回楼主丢失的头像
try {
let lzavatar = tie.querySelector(".avatar");
lzavatar.style.backgroundImage = `url("${lzavatar.getAttribute("data-src")}")`
} catch (e) { console.error(e); }
// 尝试复原发帖内容的字体样式
try {
let textContent = tie.querySelector(".thread-text");
textContent.style.cssText = `
margin-top: 0.18rem;
font-size: 0.16rem;
line-height: 0.28rem;
`
} catch (e) { console.error(e); }
let replySwitch = document.querySelector("#replySwitch"); // 标题下方的分割
replySwitch.parentNode.insertBefore(tie, replySwitch);
} catch (e) { console.error(e); }
// 尝试复原标题样式
try {
let threadtitle = document.querySelector(".thread-title");
threadtitle.style.cssText = `
margin-bottom: 0.13rem;
font-size: 0.22rem;
font-weight: 700;
line-height: 0.33rem;
`
// 置顶标题显示
if (isFixedTitle) {
let threadtitleclone = threadtitle.cloneNode(true)
threadtitle.style.visibility = "hidden";
threadtitleclone.style.cssText += `
position: fixed !important;
z-index: 99 !important;
opacity: 0.8 !important;
background-color: #FFFFFF !important;
`
threadtitle.parentNode.insertBefore(threadtitleclone, threadtitle)
}
} catch (e) { console.error(e); }
})
}
// 解析传过来的PBDATA的json
function parsePbData(responseText) {
DEBUGLOG("yyds")
let data = JSON.parse(responseText).data;
let post_list = data.post_list;
// 获取楼主id
try {
lzId = post_list[0].author.id;
} catch(e){console.error(e)}
// 获取楼层信息
for (let i = 0; i < post_list.length; i++) {
let d = post_list[i];
floorDataList[d.floor] = d;
}
// 获取内部参数
currentUrlInfo.tid = data.forum.id;
}
(function main() {
// 删减多余的打开APP的按钮,减少遮挡
GM_addStyle(`
.comment-box, .only-lz, .nav-bar-bottom, .open-app, .more-image-desc {
display: none !important;
}
`);
// 隐藏原有的打开楼中楼App按钮
GM_addStyle(`
.open-app-text {
display: none !important;
}
.open-app-text-real {
display: block !important;
-webkit-box-flex: 0;
-webkit-flex: none;
-ms-flex: none;
flex: none;
font-size: .13rem;
color: #614ec2;
}
@keyframes rotate {
0%{-webkit-transform:rotate3d(1, 0, 0, 0deg);}
25%{-webkit-transform:rotate3d(1, 0, 0, 90deg);}
50%{-webkit-transform:rotate3d(1, 0, 0, 180deg);}
75%{-webkit-transform:rotate3d(1, 0, 0, 270deg);}
100%{-webkit-transform:rotate3d(1, 0, 0, 360deg);}
}
.open-app-text-real.loading { animation: rotate 0.5s linear infinite; }
`);
(function() {
let oldXHR = unsafeWindow.XMLHttpRequest;
// 监听楼层加载的网络事件
unsafeWindow.XMLHttpRequest = function() {
let realXHR = new oldXHR();
realXHR.addEventListener('readystatechange', function() {
DEBUGLOG(realXHR.responseURL, "realXHR.responseURL")
if(REG_URL_PBDATA.test(realXHR.responseURL) && realXHR.response != "") {
parsePbData(realXHR.response);
}
}, false);
return realXHR;
}
})();
unsafeWindow.onscroll = function () {
checkUrlHashChange();
if (getPageType() == PAGE_TYPE.POSTPAGE) {
if(unsafeWindow.pageYOffset != 0) {
// 记录当前滚动位置
scrollPos = unsafeWindow.pageYOffset;
//DEBUGLOG(scrollPos, "scrollPos");
}
}
}
unsafeWindow.onhashchange = function(e) {
checkUrlHashChange();
}
switch (getPageType()) {
case PAGE_TYPE.MAINPAGE:
waitElementLoaded(".forum-block", (tiebaNameBtn) => {
tiebaName = tiebaNameBtn; // 获取吧名
tie = document.querySelector(".main-thread-content"); // 获取楼主发帖内容
let postbtn = document.querySelector(".post-page-entry-btn"); // 展开评论页的按钮
// 点击展开按钮
try {
postbtn.click();
// 手动触发页面刷新检测
checkUrlHashChange();
} catch(e){
DEBUGLOG(postbtn, "postbtn");
// 楼层数太少,不会跳转,强制手动触发页面刷新检测
// 页面开始加载时监听会大概率失效,所以这里只能主动触发抓取楼层信息的请求,发起后交给监听程序
let url = `${unsafeWindow.origin}/mg/p/getPbData?kz=${RegExp(`${unsafeWindow.origin}/p/([0-9]*)?#?/?`, 'i').exec(unsafeWindow.location.href)[1].trim()}&obj_param2=firefox&format=json&eqid=&refer=&pn=1&rn=5`;
DEBUGLOG(url, "url");
GM_xmlhttpRequest({
method: "get",
url: url,
onload: function (details) {
DEBUGLOG(details.responseText, "GM_xmlhttpRequest onload");
parsePbData(details.responseText);
checkUrlHashChange(true);
},
onerror: function (details) {
},
});
}
})
break;
case PAGE_TYPE.POSTPAGE:
// 如果当前刷新加载的是评论页,则需要先打开主页面获取数据
unsafeWindow.location.hash = "";
unsafeWindow.location.reload();
break;
case PAGE_TYPE.FLOORREPLAYPAGE:
break;
default:
break;
}
})()
})();