在帖子的导航栏添加[显示全部]按钮, 列出帖子的所有内容
// ==UserScript== // @name 列出S1一条帖子的所有内容 // @namespace https://github.com/ipcjs // @version 1.3.2 // @description 在帖子的导航栏添加[显示全部]按钮, 列出帖子的所有内容 // @author ipcjs // @include *://bbs.saraba1st.com/2b/thread-*-*-*.html // @include *://bbs.saraba1st.com/2b/forum.php* // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @grant GM.setClipboard // @grant GM_setClipboard // @grant GM_addStyle // @grant GM.addStyle // @grant unsafeWindow // @connect bbs.saraba1st.com // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js // @run-at document-start // ==/UserScript== // type, props, children // type, props, innerHTML // 'text', text const util_ui_element_creator = (type, props, children) => { let elem = null; if (type === "text") { return document.createTextNode(props); } else { elem = document.createElement(type); } for (let n in props) { if (n === "style") { for (let x in props.style) { elem.style[x] = props.style[x]; } } else if (n === "className") { elem.className = props[n]; } else if (n === "event") { for (let x in props.event) { elem.addEventListener(x, props.event[x]); } } else { elem.setAttribute(n, props[n]); } } if (children) { if (typeof children === 'string') { elem.innerHTML = children; } else { for (let i = 0; i < children.length; i++) { if (children[i] != null) elem.appendChild(children[i]); } } } return elem; } const _ = util_ui_element_creator function log(...args) { console.log(...args); } class AjaxException extends Error { constructor(resp, message = "") { super(message) this.resp = resp } toString() { return `AjaxException: message=${this.message}, status=${this.resp.status}, statusText=${this.resp.statusText}` } } function ajaxPromise(options) { return new Promise((resolve, reject) => { options.method = options.method || 'GET'; options.onload = function (resp) { resolve(resp); } options.onerror = function (resp) { reject(new AjaxException(resp)); }; options.ontimeout = function (resp) { reject(new AjaxException(resp, 'timeout')); } GM.xmlHttpRequest(options); }); } class Table { constructor() { const $postList = document.getElementById('postlist') $postList.innerHTML = '' this.listSize = 0 this.title = '' document.getElementById('ct').insertBefore(_('div', {}, [ this.$title = _('h1', {}, this.title), this.$table = _('table', { id: 'ssap-table' }), this.$msg = _('div', { id: 'ssap-msg' }) ]), $postList) } appendPostList(list) { this.append(list, [ { name: 'number', func: item => `<a target="_blank" href='forum.php?mod=redirect&goto=findpost&ptid=${item.ptid}&pid=${item.pid}'>${item.number}</a>` }, 'username', 'dateline', 'message' ]) } // append([{ name: 'ipcjs', age: 17 }, { name: '####', age: 1 }], ['name', 'age']); append(list, colNames) { this.setListSize(this.listSize + list.length) list.forEach(item => { let $tr = _('tr') colNames.forEach(it => { let name = typeof it === 'string' ? it : it.name let func = typeof it === 'string' ? item => item[name] : it.func $tr.appendChild(_('td', { className: `ssap-${name}` }, func(item))) }) this.$table.appendChild($tr) }) } setListSize(listSize) { this.listSize = listSize this._refreshTitle() } setTitle(title) { this.title = title this._refreshTitle() } showMsg(msg) { this.$msg.innerText = msg } _refreshTitle() { this.$title.innerHTML = `${this.title || 'Title'} ${this.listSize}` } _clearTable() { this.listSize = 0 this.$table.innerHTML = '' } } ////////////////////// main /////////////////////////////// let group, filter; if (!(group = /thread-(\d+)-(\d+)-(\d+)/.exec(location.pathname)) && !(group = /tid=(\d+)/.exec(location.search))) { return; // 不匹配则返回 } const POST_PAGE_MAX_COUNT = 1000; // 一次最多拉取多少条 const CONCURRENT_COUNT_MAX = 10; // 一次最多拉取多少页 const TID = group[1]; let table; switch (TID) { case '1494926': filter = f_1494926; break; default: filter = f_all; break; } GM.addStyle(` #ssap-table tr { border-top: 1px solid #888; } #ssap-msg { text-align: center; } #load-all-post { margin: 0px 10px; } #ssap-table { width: 100%; table-layout: fixed; } #ssap-table .ssap-number { width: 2%; } #ssap-table .ssap-username { width: 5%; } #ssap-table .ssap-dateline { width: 5%; } #ssap-table .ssap-message { width: 88%; } #ssap-table img { max-width: 88%; } `) function loadAllPost() { if (loadAllPost.loading) { return } loadAllPost.loading = true if (!table) { table = new Table() } const load = async function () { let page = 1; let concurrentCount = 1; while (true) { table.showMsg(`加载第${page}->${page + concurrentCount - 1}页中...`) const r###lts = await Promise.all(Array.from({ length: concurrentCount }, (v, index) => retry(async (i) => { const resp = await ajaxPromise({ url: `https://bbs.saraba1st.com/2b/api/mobile/index.php?module=viewthread&ppp=${POST_PAGE_MAX_COUNT}&tid=${TID}&page=${page + index}&version=1`, timeout: 1000 * 15 * (i + 1), }) return [index, resp] }, 3))) let json for (const [index, resp] of r###lts) { const currentPage = page + index json = JSON.parse(resp.responseText) if (currentPage === 1) { table.setTitle(json.Variables.thread.subject) } table.appendPostList(json.Variables.postlist.filter(filter)) log('>>', currentPage, table.listSize, json.Variables.thread); } // 总post条数为replies + 1 const postCount = +json.Variables.thread.replies + 1 if (table.listSize < postCount) { page += concurrentCount; concurrentCount = Math.min(concurrentCount + 3, Math.ceil((postCount - table.listSize) / POST_PAGE_MAX_COUNT), CONCURRENT_COUNT_MAX) } else { break; } } } load() .then(r => { table.showMsg('') }) .catch((e) => { table.showMsg(e.toString()) console.error(e) }) .finally(() => { loadAllPost.loading = false }) } async function retry(block, count = 3, timeMs = 1000) { let error for (let i = 0; i < count; i++) { try { return await block(i) } catch (e) { error = e await delay(timeMs) log(`retry: i=${i}, e=${e.toString()}`) } } throw error } function delay(timeMs) { return new Promise((resolve, reject) => { setTimeout(() => { resolve("continue") }, timeMs); }) } function f_1494926(item) { if (item.username === 'ipcjs') { return true; } else if (['SUNSUN', '蒹葭公子', '木水风铃'].includes(item.username) && item.message.includes('ipcjs 发表于')) { return true; } return false; } function f_all() { return true; } function feature_show_all() { document.querySelector('#pt > div.z').appendChild(_('a', { id: 'load-all-post', href: 'javascript:;', event: { click: () => loadAllPost() } }, '[显示全部]')); } function feature_voters() { const SEPARATOR = '\t' async function copyVoters() { const $button = this const $content = document.getElementById('fwin_content_viewvote') /** @type {HTMLSelectElement} */ const $select = $content.querySelector('select.ps') let r###lt = '# 投票结果' for (const [index, option] of Array.from($select.options).entries()) { $button.textContent = `[复制中(${index}/${$select.options.length})...]` r###lt += `\n\n## ${option.text}` log(option.value, option.text) let page = 0 let $next do { page++ const resp = await ajaxPromise({ url: `https://bbs.saraba1st.com/2b/forum.php?mod=misc&action=viewvote&tid=${TID}&polloptionid=${option.value}&infloat=yes&handlekey=viewvote&page=${page}&inajax=1&ajaxtarget=fwin_content_viewvote` }) const $xml = new DOMParser().parseFromString(resp.responseXML.documentElement.firstChild.textContent, 'text/html') const $voters = Array.from($xml.querySelectorAll('li > p > a')) r###lt += page > 1 ? SEPARATOR : '\n\n' r###lt += $voters.map(it => it.textContent).join(SEPARATOR) $next = $xml.querySelector('.pg > .nxt') } while ($next) } log(r###lt) GM.setClipboard(r###lt) $button.textContent = '[复制完成!]' } new MutationObserver((mutations, observer) => { for (let m of mutations) { for (let node of m.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { /** @type {HTMLDivElement} */ const $title = node.id === 'fctrl_viewvote' ? node : node.querySelector('#fctrl_viewvote') let $button = node.querySelector('#ssap_copy_voters') if ($title && !$button) { log($title) $button = _('a', { id: 'ssap_copy_voters', href: 'javascript:;', event: { click: copyVoters } }, '[复制结果]') $title.insertBefore($button, $title.lastChild) } } } } }).observe(document.getElementById('append_parent'), { childList: true, subtree: true, }) } unsafeWindow.addEventListener('DOMContentLoaded', (event) => { feature_show_all() feature_voters() })