列出S1一条帖子的所有内容

在帖子的导航栏添加[显示全部]按钮, 列出帖子的所有内容

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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: 'fuck', 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 results = 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 results) {
                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 result = '# 投票结果'
        for (const [index, option] of Array.from($select.options).entries()) {
            $button.textContent = `[复制中(${index}/${$select.options.length})...]`
            result += `\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'))

                result += page > 1 ? SEPARATOR : '\n\n'
                result += $voters.map(it => it.textContent).join(SEPARATOR)

                $next = $xml.querySelector('.pg > .nxt')
            } while ($next)

        }
        log(result)
        GM.setClipboard(result)
        $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()
})