Greasy Fork is available in English.
遍历帖子所有界面并自动展开折叠内容,然后重新组织为楼中楼形式
// ==UserScript==
// @name         NGA 楼中楼
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  遍历帖子所有界面并自动展开折叠内容,然后重新组织为楼中楼形式
// @author       cloud_rider
// @match        https://bbs.nga.cn/read.php?tid=*
// @match        https://ngabbs.com/read.php?tid=*
// @match        https://nga.178.com/read.php?tid=*
// @grant        GM_openInTab
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @license      MIT
// ==/UserScript==
(function() {
    'use strict';
    console.log('[NGA 楼中楼] 脚本启动');
    const urlParams = new URLSearchParams(window.location.search);
    const isLoaderTab = urlParams.get('loader') === '1';
    const loadedPages = new Set();
    let progressBar = null, progressLine2 = null;
    let isFinished = false;
    // 展开折叠内容(简化版)
    function expandAllCollapses(container) {
        try {
            const buttons = container.querySelectorAll('button[name="collapseSwitchButton"]');
            console.log('[NGA 自动展开] 找到', buttons.length, '个折叠按钮');
            let expanded = 0;
            buttons.forEach(button => {
                try {
                    if (button.textContent === '+') {
                        button.click();
                        button.textContent = '-';
                        expanded++;
                        console.log('[NGA 自动展开] 展开第', expanded, '个折叠');
                    }
                } catch (e) {
                    console.warn('[NGA 自动展开] 按钮点击失败:', e);
                    // 后备方案:直接修改 DOM
                    const collapseDiv = button.parentNode.nextSibling;
                    if (collapseDiv && collapseDiv.classList.contains('collapse') && collapseDiv.style.display === 'none') {
                        collapseDiv.style.display = 'block';
                        button.textContent = '-';
                        expanded++;
                        console.log('[NGA 自动展开] 强制展开第', expanded, '个折叠');
                    }
                }
            });
            console.log('[NGA 自动展开] 完成展开', expanded, '个折叠');
        } catch (e) {
            console.error('[NGA 自动展开] 展开折叠失败:', e);
        }
    }
    // === 楼中楼逻辑 ===
    function createProgressBar() {
        progressBar = document.createElement('div');
        progressBar.style.cssText = `position:fixed;top:20px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:white;padding:12px 20px;border-radius:12px;font-size:15px;font-weight:bold;text-align:center;z-index:9999;min-width:300px;box-shadow:0 4px 12px rgba(0,0,0,0.3);`;
        const line1 = document.createElement('div'); line1.textContent = '楼中楼脚本正在运行,请稍候'; line1.style.marginBottom = '6px';
        progressLine2 = document.createElement('div'); progressLine2.textContent = '正在初始化...'; progressLine2.style.fontWeight = 'normal'; progressLine2.style.fontSize = '14px';
        progressBar.appendChild(line1); progressBar.appendChild(progressLine2); document.body.appendChild(progressBar);
    }
    function updateProgressLine2(t) { if (progressLine2) progressLine2.textContent = t; }
    function removeProgressBar() { if (progressBar) { progressBar.style.opacity = '0'; setTimeout(() => progressBar?.parentNode?.removeChild(progressBar), 500); } }
    function showInitialProgress() {
        try {
            createProgressBar();
            const info = parsePageInfo();
            updateProgressLine2(`正在加载:第 ${info.currentPage} 页 / 共 ${info.totalPages} 页`);
            const container = document.getElementById('m_posts_c');
            if (container) {
                expandAllCollapses(container);
            } else {
                console.warn('[NGA 楼中楼] 未找到 m_posts_c 容器');
            }
        } catch (e) {
            console.error('[NGA 楼中楼] showInitialProgress 错误:', e);
        }
    }
    if (document.readyState === 'loading') {
        console.log('[NGA 楼中楼] 页面加载中,等待 DOMContentLoaded');
        document.addEventListener('DOMContentLoaded', showInitialProgress);
    } else {
        console.log('[NGA 楼中楼] 页面已加载,直接执行 showInitialProgress');
        showInitialProgress();
    }
    if (!isLoaderTab) {
        setTimeout(startLoading, 8000);
    } else {
        setTimeout(autoClickJump, 2000);
    }
    function startLoading() {
        try {
            const tid = urlParams.get('tid');
            if (!tid) {
                console.warn('[NGA 楼中楼] 未找到 tid 参数');
                updateProgressLine2('错误:未找到帖子 ID');
                setTimeout(removeProgressBar, 3000);
                return;
            }
            const info = parsePageInfo();
            if (info.totalPages <= info.currentPage) {
                console.log('[NGA 楼中楼] 已最后一页');
                updateProgressLine2('已是最后一页');
                setTimeout(removeProgressBar, 2000);
                return;
            }
            const container = document.getElementById('m_posts_c');
            if (!container) {
                console.warn('[NGA 楼中楼] 未找到 m_posts_c 容器');
                updateProgressLine2('错误:未找到容器');
                setTimeout(removeProgressBar, 3000);
                return;
            }
            loadedPages.clear();
            updateProgressLine2(`正在加载:第 ${info.currentPage + 1} 页 / 共 ${info.totalPages} 页`);
            loadNextPage(info.currentPage + 1, info.totalPages, tid, container);
        } catch (e) {
            console.error('[NGA 楼中楼] startLoading 错误:', e);
        }
    }
    function parsePageInfo() {
        try {
            let totalPages = 1;
            let currentPage = parseInt(urlParams.get('page')) || 1;
            const links = document.querySelectorAll('#pagebtop a, #pagebbtm a');
            links.forEach(link => {
                const text = link.textContent.trim();
                const num = parseInt(text);
                if (!isNaN(num)) totalPages = Math.max(totalPages, num);
            });
            const lastPageLink = document.querySelector('#pagebtop a[title*="最后页"], #pagebbtm a[title*="最后页"]');
            if (lastPageLink) {
                const match = lastPageLink.href.match(/page=(\d+)/);
                if (match) {
                    const lastPage = parseInt(match[1]);
                    if (lastPage > totalPages) totalPages = lastPage;
                }
            }
            console.log('[NGA 楼中楼] 解析页面: 当前页', currentPage, '总页数', totalPages);
            return { totalPages, currentPage };
        } catch (e) {
            console.error('[NGA 楼中楼] parsePageInfo 错误:', e);
            return { totalPages: 1, currentPage: 1 };
        }
    }
    function loadNextPage(page, total, tid, container) {
        try {
            if (loadedPages.has(page)) {
                page < total ? loadNextPage(page + 1, total, tid, container) : finishLoading(container);
                return;
            }
            updateProgressLine2(`正在加载:第 ${page} 页 / 共 ${total} 页`);
            const url = `${window.location.origin}/read.php?tid=${tid}&loader=1&page=${page}`;
            GM_openInTab(url, { active: false });
            const key = `POSTS_${page}`;
            const check = setInterval(() => {
                const html = GM_getValue(key);
                if (html) {
                    clearInterval(check);
                    GM_setValue(key, null);
                    appendPosts(html, page, container);
                    loadedPages.add(page);
                    page < total ? loadNextPage(page + 1, total, tid, container) : finishLoading(container);
                }
            }, 1000);
            setTimeout(() => {
                if (!loadedPages.has(page)) {
                    clearInterval(check);
                    loadedPages.add(page);
                    page < total ? loadNextPage(page + 1, total, tid, container) : finishLoading(container);
                }
            }, 30000);
        } catch (e) {
            console.error('[NGA 楼中楼] loadNextPage 错误:', e);
        }
    }
    function appendPosts(html, page, container) {
        try {
            const tempContainer = document.createElement('div');
            tempContainer.innerHTML = html;
            expandAllCollapses(tempContainer);
            const tables = tempContainer.querySelectorAll('table.forumbox.postbox');
            tables.forEach(t => container.appendChild(t.cloneNode(true)));
            console.log('[NGA 楼中楼] 第', page, '页追加', tables.length, '条');
        } catch (e) {
            console.error('[NGA 楼中楼] appendPosts 错误:', e);
        }
    }
    function finishLoading(container) {
        if (isFinished) return;
        isFinished = true;
        updateProgressLine2('正在构建楼中楼...');
        document.getElementById('m_pbtntop')?.remove();
        document.getElementById('m_pbtnbtm')?.remove();
        setTimeout(() => {
            try {
                enableThreadedView();
                expandAllCollapses(container);
            } catch (e) {
                console.error('[NGA 楼中楼] finishLoading 错误:', e);
            }
        }, 1000);
    }
    function autoClickJump() {
        try {
            if (document.body.innerHTML.includes('访客不能直接访问')) {
                if (window.g) { window.g(); return; }
                const link = document.querySelector('a[onclick="g()"]');
                if (link) { link.click(); setTimeout(extractPosts, 2000); return; }
                setAntiBotCookie();
                return;
            }
            extractPosts();
        } catch (e) {
            console.error('[NGA 楼中楼] autoClickJump 错误:', e);
        }
    }
    function setAntiBotCookie() {
        try {
            const now = Date.now();
            document.cookie = `guestJs=${Math.floor(now/1000)}_9c1cuj;domain=bbs.nga.cn;path=/;max-age=1800`;
            document.cookie = `lastpath=0;domain=bbs.nga.cn;path=/;max-age=0`;
            const url = new URL(window.location.href);
            url.searchParams.set('rand', Math.floor(Math.random() * 1000));
            setTimeout(() => window.location.replace(url.toString()), 300);
        } catch (e) {
            console.error('[NGA 楼中楼] setAntiBotCookie 错误:', e);
        }
    }
    function extractPosts() {
        try {
            const page = new URLSearchParams(window.location.search).get('page') || '1';
            const container = document.getElementById('m_posts_c');
            if (container) {
                expandAllCollapses(container);
                GM_setValue(`POSTS_${page}`, container.innerHTML);
                console.log('[NGA 楼中楼] 子页', page, '数据写入 GM_setValue');
                setTimeout(() => window.close(), 500);
            } else {
                console.warn('[NGA 楼中楼] 子页', page, '未找到 m_posts_c,稍后重试');
                setTimeout(extractPosts, 1000);
            }
        } catch (e) {
            console.error('[NGA 楼中楼] extractPosts 错误:', e);
        }
    }
    function enableThreadedView() {
        try {
            const style = document.createElement('style');
            style.textContent = `table.forumbox.postbox.indented > tbody > tr > td { background: #F2EDDF !important; }`;
            document.head.appendChild(style);
            const container = document.getElementById('m_posts_c');
            const all = Array.from(container.querySelectorAll('table.forumbox.postbox'));
            if (!all.length) {
                updateProgressLine2('无帖子');
                setTimeout(removeProgressBar, 2000);
                return;
            }
            const pidToFloor = {}, floorToPost = {};
            all.forEach(t => {
                const floor = t.querySelector('a[name^="l"]') ? parseInt(t.querySelector('a[name^="l"]').textContent.replace('#','')) || 0 : 0;
                const pid = t.querySelector('a[id^="pid"][id$="Anchor"]') ? t.querySelector('a[id^="pid"][id$="Anchor"]').id.replace('pid','').replace('Anchor','') : '0';
                pidToFloor[pid] = floor;
                floorToPost[floor] = { element: t, pid, children: [] };
            });
            let removed = 0;
            all.forEach(t => {
                const pid = t.querySelector('a[id^="pid"][id$="Anchor"]') ? t.querySelector('a[id^="pid"][id$="Anchor"]').id.replace('pid','').replace('Anchor','') : '0';
                const floor = pidToFloor[pid];
                let parent = 0, removeQuote = false;
                const quote = t.querySelector('div.quote');
                if (quote) {
                    const link = quote.querySelector('a[href*="topid="]');
                    if (link) { const p = link.href.match(/topid=(\d+)/)?.[1]; parent = p ? pidToFloor[p] : 0; removeQuote = true; removed++; }
                }
                floorToPost[floor].parentFloor = parent;
                if (parent !== floor && parent !== undefined && floorToPost[parent]) floorToPost[parent].children.push(floorToPost[floor]);
                if (removeQuote) floorToPost[floor].removeQuote = true;
            });
            const root = floorToPost[0];
            if (!root) {
                updateProgressLine2('错误:无主楼');
                setTimeout(removeProgressBar, 3000);
                return;
            }
            function sort(node) { if (node.children) { node.children.sort((a,b)=>a.floor-b.floor); node.children.forEach(sort); } }
            sort(root);
            renderThreadedView(root, container);
            updateProgressLine2(`完成!共 ${all.length} 条`);
            setTimeout(removeProgressBar, 3000);
        } catch (e) {
            console.error('[NGA 楼中楼] enableThreadedView 错误:', e);
        }
    }
    function renderThreadedView(root, container) {
        try {
            container.innerHTML = '';
            function render(post, level = 0) {
                const dl = (post.floor === 0 || post.parentFloor === 0) ? 0 : level;
                if (post.removeQuote) { const q = post.element.querySelector('div.quote'); if (q) q.remove(); }
                if (dl > 0) {
                    const c1 = post.element.querySelector('td.c1');
                    if (c1) { const info = c1.querySelector('div[style*="text-align:left;line-height:1.5em"]'); if (info) { c1.innerHTML = ''; c1.appendChild(info.cloneNode(true)); } }
                }
                if (dl > 0) {
                    const c2 = post.element.querySelector('td.c2');
                    if (c2) {
                        ['.postInfo', '.goodbad', `[id^="postsubject"]`, '.x'].forEach(s => {
                            const el = c2.querySelector(s);
                            if (el) {
                                if (s.includes('postsubject') && el.textContent.trim() === '') el.style.display = 'none';
                                else if (s !== '.postInfo') el.style.display = 'none';
                                else { el.style.lineHeight = '1.2'; el.style.margin = '2px 0'; }
                            }
                        });
                        const content = c2.querySelector(`[id^="postcontent"]`);
                        if (content) { content.style.margin = '4px 0'; content.style.lineHeight = '1.45'; }
                    }
                }
                if (dl > 0) {
                    post.element.classList.add('indented');
                    const t = post.element;
                    t.style.border = '1px solid #fff';
                    t.style.borderRadius = '6px';
                    t.style.overflow = 'hidden';
                    t.style.boxShadow = '0 1px 3px rgba(0,0,0,0.08)';
                }
                const wrapper = document.createElement('div');
                wrapper.style.marginLeft = `${dl * 20}px`;
                wrapper.style.marginBottom = '8px';
                wrapper.appendChild(post.element.cloneNode(true));
                container.appendChild(wrapper);
                post.children.forEach(c => render(c, level + 1));
            }
            render(root, 0);
        } catch (e) {
            console.error('[NGA 楼中楼] renderThreadedView 错误:', e);
        }
    }
})();