GitHub Discussion Load-More & Expand Collapsed Auto-Loader

Automatically clicks "Load more" on GitHub discussion pages until all comments are loaded AND expands all collapsed comments

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         GitHub Discussion Load-More & Expand Collapsed Auto-Loader
// @namespace    http://tampermonkey.net/
// @version      1.3.1
// @description  Automatically clicks "Load more" on GitHub discussion pages until all comments are loaded AND expands all collapsed comments
// @author       Premysl Karbula (https://github.com/smuuf)
// @license      MIT
// @match        https://github.com/orgs/*/discussions/*
// @match        https://github.com/*/*/discussions/*
// @match        https://github.com/*/*/issues/*
// @match        https://github.com/*/*/pulls/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    let loadMoreRunning = false;
    let expandRunning = false;
    let loadIntervalId = null;
    let expandIntervalId = null;
    let clickCount = 0;
    let expandCount = 0;
    
    const CLICK_DELAY_MS = 1200;
    const PANEL_ID = 'gh-autoloader-panel-' + Math.random().toString(36).substr(2, 6);

    // --- Find the "Load more" button ---
    function findLoadMoreButton() {
        // Primary: ajax-pagination-btn class (matches the HTML you provided)
        const ajaxBtn = document.querySelector('.ajax-pagination-btn');
        if (ajaxBtn && ajaxBtn.offsetParent !== null) return ajaxBtn;

        // Fallback: any button/link containing "Load more" text
        const allButtons = document.querySelectorAll(`button:not(#${PANEL_ID} *), a:not(#${PANEL_ID} *)`);
        for (const el of allButtons) {
            if (/load more/i.test(el.textContent) && el.offsetParent !== null) {
                return el;
            }
        }
        return null;
    }

    // --- Find and click all collapsed comment buttons ---
    function expandCollapsedComments() {
        let expanded = 0;
        
        // Method 1: Look for buttons with octicon-unfold icon
        const unfoldButtons = document.querySelectorAll('button svg.octicon-unfold');
        unfoldButtons.forEach(svg => {
            const button = svg.closest('button');
            if (button && button.offsetParent !== null && !button.disabled) {
                button.click();
                expanded++;
            }
        });

        // Method 2: Look for buttons with tooltip text "show comment"
        const tooltips = document.querySelectorAll('[id*="_r_"]');
        tooltips.forEach(tooltip => {
            if (tooltip.textContent.toLowerCase().includes('show comment')) {
                const buttonId = tooltip.getAttribute('aria-labelledby') || tooltip.id;
                if (buttonId) {
                    const button = document.querySelector(`[aria-labelledby="${buttonId}"]`);
                    if (button && button.offsetParent !== null && !button.disabled) {
                        button.click();
                        expanded++;
                    }
                }
            }
        });

        // Method 3: Look for buttons in CommentActions with unfold icon
        const commentActionButtons = document.querySelectorAll('.CommentActions-module__CommentActionsIconButton__Fsg4J');
        commentActionButtons.forEach(button => {
            const unfoldIcon = button.querySelector('.octicon-unfold');
            if (unfoldIcon && button.offsetParent !== null && !button.disabled) {
                button.click();
                expanded++;
            }
        });

        return expanded;
    }

    // --- Status text helpers ---
    function statusText() {
        const details = [];
        
        if (loadMoreRunning) {
            details.push(`Loading… (${clickCount})`);
        } else if (clickCount > 0) {
            details.push(`${clickCount} loads`);
        }
        
        if (expandRunning) {
            details.push(`Expanding… (${expandCount})`);
        } else if (expandCount > 0) {
            details.push(`${expandCount} expanded`);
        }
        
        return details.length > 0 ? details.join(' | ') : 'Idle';
    }

    // --- Core loop for Load More ---
    function tickLoadMore() {
        const btn = findLoadMoreButton();
        if (btn) {
            btn.click();
            clickCount++;
            updateUI();
        } else {
            // No more "Load more" buttons
            stopLoadMore(true);
        }
    }

    // --- Core loop for Expand Collapsed ---
    function tickExpand() {
        const expanded = expandCollapsedComments();
        if (expanded > 0) {
            expandCount += expanded;
            updateUI();
        } else {
            // No more collapsed comments
            stopExpand(true);
        }
    }

    function startLoadMore() {
        if (loadMoreRunning) return;
        loadMoreRunning = true;
        clickCount = 0;
        updateUI();
        tickLoadMore();
        loadIntervalId = setInterval(tickLoadMore, CLICK_DELAY_MS);
    }

    function stopLoadMore(auto = false) {
        if (!loadMoreRunning && !auto) return;
        loadMoreRunning = false;
        clearInterval(loadIntervalId);
        loadIntervalId = null;
        updateUI();
    }

    function startExpand() {
        if (expandRunning) return;
        expandRunning = true;
        expandCount = 0;
        updateUI();
        tickExpand();
        expandIntervalId = setInterval(tickExpand, CLICK_DELAY_MS);
    }

    function stopExpand(auto = false) {
        if (!expandRunning && !auto) return;
        expandRunning = false;
        clearInterval(expandIntervalId);
        expandIntervalId = null;
        updateUI();
    }

    function startBoth() {
        startLoadMore();
        startExpand();
    }

    function stopBoth() {
        stopLoadMore();
        stopExpand();
    }

    // --- Build floating UI ---
    const panel = document.createElement('div');
    panel.id = PANEL_ID
    panel.style.cssText = `
        position: fixed;
        bottom: 24px;
        right: 24px;
        z-index: 99999;
        background: #161b22;
        border: 1px solid #30363d;
        border-radius: 10px;
        padding: 12px 16px;
        display: flex;
        flex-direction: column;
        gap: 8px;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
        font-size: 13px;
        color: #e6edf3;
        box-shadow: 0 8px 24px rgba(0,0,0,0.4);
        min-width: 200px;
        user-select: none;
    `;

    const title = document.createElement('div');
    title.textContent = '⚡ Discussion Auto-Loader';
    title.style.cssText = 'font-weight: 600; font-size: 13px; color: #58a6ff;';

    const label = document.createElement('div');
    label.style.cssText = 'color: #8b949e; font-size: 12px;';
    label.textContent = statusText();

    const btnRow = document.createElement('div');
    btnRow.style.cssText = 'display: flex; gap: 8px;';

    // Add hover styles
    const styleSheet = document.createElement('style');
    styleSheet.textContent = `
        .gh-autoloader-btn {
            flex: 1;
            padding: 8px 12px;
            border-radius: 6px;
            font-size: 12px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s ease;
            box-shadow: 0 1px 3px rgba(0,0,0,0.12);
            white-space: nowrap;
        }
        .gh-autoloader-btn:hover:not(:disabled) {
            transform: translateY(-1px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
        }
        .gh-autoloader-btn:active:not(:disabled) {
            transform: translateY(0);
            box-shadow: 0 1px 3px rgba(0,0,0,0.12);
        }
        .gh-autoloader-btn:disabled {
            cursor: not-allowed;
            opacity: 0.5;
        }
        .gh-autoloader-btn-load {
            border: 1px solid #238636;
            background: linear-gradient(180deg, #2ea043 0%, #238636 100%);
            color: #fff;
        }
        .gh-autoloader-btn-load:hover:not(:disabled) {
            background: linear-gradient(180deg, #3fb950 0%, #2ea043 100%);
            border-color: #2ea043;
        }
        .gh-autoloader-btn-expand {
            border: 1px solid #1f6feb;
            background: linear-gradient(180deg, #388bfd 0%, #1f6feb 100%);
            color: #fff;
        }
        .gh-autoloader-btn-expand:hover:not(:disabled) {
            background: linear-gradient(180deg, #539bf5 0%, #388bfd 100%);
            border-color: #388bfd;
        }
        .gh-autoloader-btn-both {
            border: 1px solid #a371f7;
            background: linear-gradient(180deg, #b083f0 0%, #a371f7 100%);
            color: #fff;
        }
        .gh-autoloader-btn-both:hover:not(:disabled) {
            background: linear-gradient(180deg, #bc8cff 0%, #b083f0 100%);
            border-color: #b083f0;
        }
        .gh-autoloader-btn-stop {
            border: 1px solid #30363d;
            background: linear-gradient(180deg, #21262d 0%, #161b22 100%);
            color: #8b949e;
        }
        .gh-autoloader-btn-stop:hover:not(:disabled) {
            background: linear-gradient(180deg, #30363d 0%, #21262d 100%);
            border-color: #484f58;
            color: #c9d1d9;
        }
        .gh-autoloader-btn-stop.active {
            border-color: #da3633;
            background: linear-gradient(180deg, #b62324 0%, #8e1b1d 100%);
            color: #ffdcd7;
        }
        .gh-autoloader-btn-stop.active:hover:not(:disabled) {
            background: linear-gradient(180deg, #da3633 0%, #b62324 100%);
            border-color: #f85149;
        }
    `;
    document.head.appendChild(styleSheet);

    const loadMoreBtn = document.createElement('button');
    loadMoreBtn.textContent = '▶ Load More';
    loadMoreBtn.className = 'gh-autoloader-btn gh-autoloader-btn-load';

    const expandBtn = document.createElement('button');
    expandBtn.textContent = '↕ Expand';
    expandBtn.className = 'gh-autoloader-btn gh-autoloader-btn-expand';

    const btnRow2 = document.createElement('div');
    btnRow2.style.cssText = 'display: flex; gap: 8px;';

    const startBothBtn = document.createElement('button');
    startBothBtn.textContent = '▶ Both';
    startBothBtn.className = 'gh-autoloader-btn gh-autoloader-btn-both';

    const stopBtn = document.createElement('button');
    stopBtn.textContent = '⏹ Stop All';
    stopBtn.className = 'gh-autoloader-btn gh-autoloader-btn-stop';

    const delayRow = document.createElement('div');
    delayRow.style.cssText = 'display: flex; align-items: center; gap: 8px; color: #8b949e;';

    const delayLabel = document.createElement('label');
    delayLabel.textContent = 'Delay (ms):';
    delayLabel.style.fontSize = '12px';

    const delayInput = document.createElement('input');
    delayInput.type = 'number';
    delayInput.value = CLICK_DELAY_MS;
    delayInput.min = 300;
    delayInput.max = 10000;
    delayInput.step = 100;
    delayInput.style.cssText = `
        width: 70px;
        background: #0d1117;
        border: 1px solid #30363d;
        border-radius: 6px;
        color: #e6edf3;
        padding: 3px 6px;
        font-size: 12px;
    `;

    // Drag support
    let dragging = false, ox = 0, oy = 0;
    title.style.cursor = 'grab';
    title.addEventListener('mousedown', e => {
        dragging = true;
        const r = panel.getBoundingClientRect();
        ox = e.clientX - r.left;
        oy = e.clientY - r.top;
        title.style.cursor = 'grabbing';
    });
    document.addEventListener('mousemove', e => {
        if (!dragging) return;
        panel.style.left = (e.clientX - ox) + 'px';
        panel.style.top = (e.clientY - oy) + 'px';
        panel.style.right = 'auto';
        panel.style.bottom = 'auto';
    });
    document.addEventListener('mouseup', () => {
        dragging = false;
        title.style.cursor = 'grab';
    });

    function updateUI() {
        label.textContent = statusText();
        
        loadMoreBtn.disabled = loadMoreRunning;
        expandBtn.disabled = expandRunning;
        startBothBtn.disabled = loadMoreRunning || expandRunning;
        
        const anyRunning = loadMoreRunning || expandRunning;
        if (anyRunning) {
            stopBtn.classList.add('active');
        } else {
            stopBtn.classList.remove('active');
        }
    }

    loadMoreBtn.addEventListener('click', () => {
        clickCount = 0;
        startLoadMore();
    });

    expandBtn.addEventListener('click', () => {
        expandCount = 0;
        startExpand();
    });

    startBothBtn.addEventListener('click', () => {
        clickCount = 0;
        expandCount = 0;
        startBoth();
    });

    stopBtn.addEventListener('click', () => stopBoth());

    delayRow.append(delayLabel, delayInput);
    btnRow.append(loadMoreBtn, expandBtn);
    btnRow2.append(startBothBtn, stopBtn);
    panel.append(title, label, btnRow, btnRow2, delayRow);
    document.body.appendChild(panel);

    updateUI();

})();