// ==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 = `${content.text}` 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; } })() })();