MyDealz | Load All Comment Pages & Expand All Replies (English)

Load all comments and auto-expand replies on MyDealz with continuous monitoring (English UI)

// ==UserScript==
// @name         MyDealz | Load All Comment Pages & Expand All Replies (English)
// @namespace    violentmonkey
// @version      3.1.1
// @description  Load all comments and auto-expand replies on MyDealz with continuous monitoring (English UI)
// @author       piknockyou via vibe-coding
// @match        https://www.mydealz.de/deals/*
// @match        https://www.mydealz.de/diskussion/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=mydealz.de
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Shared Time Interval Parameters (in milliseconds)
    const REPLY_CHECK_INTERVAL = 500; // How often to scan when no buttons are found
    const MAX_CHECKS_WITHOUT_ACTION = 5; // How many checks without action before stopping
    const CLICK_COOLDOWN_MS = 200; // Delay after a successful click
    const PAGE_TRANSITION_DELAY = 2000; // Delay after "Next Page"
    const DOM_CHECK_INTERVAL = 100; // How often DOM elements are checked
    const DOM_TIMEOUT = 10000; // Maximum wait time for DOM elements

    // Shared Constants
    const REPLY_BUTTON_SELECTOR = 'button[data-t="moreReplies"]:not([disabled])';
    const BUTTON_DEFAULT_STYLE = `
        position: fixed;
        padding: 8px 16px;
        background-color: #007bff;
        color: white;
        border: none;
        border-radius: 5px;
        cursor: pointer;
        z-index: 9999;
        font-size: 12px;
    `;
    const BUTTON_PROCESSING_STYLE = `
        position: fixed;
        padding: 8px 16px;
        background-color: #ff0000;
        color: white;
        border: none;
        border-radius: 5px;
        cursor: not-allowed;
        z-index: 9999;
        font-size: 12px;
    `;
    const BUTTON_SUCCESS_STYLE = `
        position: fixed;
        padding: 8px 16px;
        background-color: #00ff00;
        color: black;
        border: none;
        border-radius: 5px;
        cursor: default;
        z-index: 9999;
        font-size: 12px;
    `;
    const PAGE_INDICATOR_STYLE = `
        font-size: 0.8em;
        color: #888;
        margin-left: 5px;
        font-style: italic;
    `;

    // Specific Button Positioning
    const LOAD_ALL_BUTTON_STYLE = `${BUTTON_DEFAULT_STYLE} bottom: 20px; right: 20px;`;
    const EXPAND_REPLIES_BUTTON_STYLE = `${BUTTON_DEFAULT_STYLE} bottom: 60px; right: 20px; background-color: #2C7CBD;`;
    const LOAD_ALL_PROCESSING_STYLE = `${BUTTON_PROCESSING_STYLE} bottom: 20px; right: 20px;`;
    const EXPAND_REPLIES_PROCESSING_STYLE = `${BUTTON_PROCESSING_STYLE} bottom: 60px; right: 20px; background-color: #FFA500;`;
    const LOAD_ALL_SUCCESS_STYLE = `${BUTTON_SUCCESS_STYLE} bottom: 20px; right: 20px;`;
    const EXPAND_REPLIES_SUCCESS_STYLE = `${BUTTON_SUCCESS_STYLE} bottom: 60px; right: 20px; background-color: #4CAF50;`;

    // Function to create or update a button
    function ensureButton(className, text, style, clickHandler) {
        let button = document.querySelector(`button.${className}`);
        if (!button) {
            button = document.createElement('button');
            button.className = className;
            button.textContent = text;
            button.style.cssText = style;
            button.addEventListener('click', clickHandler);
            document.body.appendChild(button);
            console.log(`${text} button added.`);
        }
        return button;
    }

    // Function to update button state
    function updateButtonState(button, state, text, defaultStyle, processingStyle, successStyle, clickCount = 0) {
        switch (state) {
            case 'processing':
                button.textContent = text || 'Processing...';
                button.style.cssText = processingStyle;
                button.disabled = true;
                break;
            case 'success':
                button.textContent = clickCount ? `Done (${clickCount} clicks)` : 'Success!';
                button.style.cssText = successStyle;
                button.disabled = true;
                break;
            default:
                button.textContent = text;
                button.style.cssText = defaultStyle;
                button.disabled = false;
        }
    }

    // Function to wait for an element to appear with a minimum child count
    function waitForElement(selector, minChildren = 1, timeout = DOM_TIMEOUT) {
        return new Promise((resolve, reject) => {
            const startTime = Date.now();
            const check = () => {
                const element = document.querySelector(selector);
                if (element && element.children.length >= minChildren) {
                    resolve(element);
                } else if (Date.now() - startTime > timeout) {
                    reject(new Error(`Timeout waiting for ${selector} with at least ${minChildren} children`));
                } else {
                    setTimeout(check, DOM_CHECK_INTERVAL);
                }
            };
            check();
        });
    }

    // Function to expand all replies on the current page with continuous monitoring
    async function expandAllReplies(button = null) {
        return new Promise((resolve) => {
            let totalClicks = 0;
            let consecutiveChecksWithoutAction = 0;

            if (button) {
                updateButtonState(
                    button,
                    'processing',
                    'Expanding...',
                    EXPAND_REPLIES_BUTTON_STYLE,
                    EXPAND_REPLIES_PROCESSING_STYLE,
                    EXPAND_REPLIES_SUCCESS_STYLE
                );
            }

            const checkAndClick = async () => {
                // Consider only visible buttons
                const buttons = Array.from(document.querySelectorAll(REPLY_BUTTON_SELECTOR))
                    .filter(btn => btn.offsetParent !== null);

                if (buttons.length > 0) {
                    consecutiveChecksWithoutAction = 0;
                    const buttonToClick = buttons[0];

                    console.log(`Clicking button: "${buttonToClick.textContent.trim()}"`);
                    try {
                        buttonToClick.click();
                        totalClicks++;
                        await new Promise(res => setTimeout(res, CLICK_COOLDOWN_MS));
                        setTimeout(checkAndClick, 0); // Check again immediately
                    } catch (e) {
                        console.error("Error clicking button:", e, buttonToClick);
                        await new Promise(res => setTimeout(res, REPLY_CHECK_INTERVAL));
                        setTimeout(checkAndClick, 0);
                    }
                } else {
                    consecutiveChecksWithoutAction++;
                    console.log(`No clickable buttons found. Check ${consecutiveChecksWithoutAction}/${MAX_CHECKS_WITHOUT_ACTION}. Waiting ${REPLY_CHECK_INTERVAL}ms...`);
                    if (consecutiveChecksWithoutAction < MAX_CHECKS_WITHOUT_ACTION) {
                        setTimeout(checkAndClick, REPLY_CHECK_INTERVAL);
                    } else {
                        console.log(`Expansion finished. Total clicks: ${totalClicks}.`);
                        const remainingButtons = document.querySelectorAll(REPLY_BUTTON_SELECTOR);
                        if (remainingButtons.length > 0) {
                            console.warn(`Warning: ${remainingButtons.length} buttons are still present. They might be hidden or require different interaction.`);
                        }
                        if (button) {
                            updateButtonState(
                                button,
                                'success',
                                '', // Text will be "Done (X clicks)"
                                EXPAND_REPLIES_BUTTON_STYLE,
                                EXPAND_REPLIES_PROCESSING_STYLE,
                                EXPAND_REPLIES_SUCCESS_STYLE,
                                totalClicks
                            );
                        }
                        resolve();
                    }
                }
            };

            checkAndClick();
        });
    }

    // Function to load all comments and expand replies
    async function loadAllCommentsAndExpandReplies() {
        const loadButton = ensureButton(
            'mydealz-load-all-button',
            'Load All Comments & Expand Replies',
            LOAD_ALL_BUTTON_STYLE,
            loadAllCommentsAndExpandReplies
        );

        // Normalize URL to base with #comments
        const baseUrl = window.location.pathname + '?#comments';
        if (window.location.href !== window.location.origin + baseUrl) {
            console.log(`Loading normalized URL: ${baseUrl}`);
            localStorage.setItem('mydealz_script_triggered', 'true');
            updateButtonState(
                loadButton,
                'processing',
                'Processing...',
                LOAD_ALL_BUTTON_STYLE,
                LOAD_ALL_PROCESSING_STYLE,
                LOAD_ALL_SUCCESS_STYLE
            );
            window.location.href = baseUrl;
            return; // Wait for reload
        }

        updateButtonState(
            loadButton,
            'processing',
            'Processing...',
            LOAD_ALL_BUTTON_STYLE,
            LOAD_ALL_PROCESSING_STYLE,
            LOAD_ALL_SUCCESS_STYLE
        );

        let masterCommentList;
        try {
            masterCommentList = await waitForElement('ol.commentList.commentList--anchored', 1);
        } catch (error) {
            console.log('No comment list found. Script cannot proceed.');
            updateButtonState(
                loadButton,
                'default',
                'Load All Comments & Expand Replies',
                LOAD_ALL_BUTTON_STYLE,
                LOAD_ALL_PROCESSING_STYLE,
                LOAD_ALL_SUCCESS_STYLE
            );
            return;
        }

        const pagination = document.querySelector('nav[role="navigation"][aria-label="Nummerierung"]');
        let totalPages = 1;
        if (pagination) {
            const pageButtons = pagination.querySelectorAll('.comments-pagi-page');
            if (pageButtons.length === 0) {
                console.log('No page buttons found in pagination. Assuming single page.');
            } else {
                const lastPageButton = pageButtons[pageButtons.length - 1];
                totalPages = parseInt(lastPageButton.textContent.trim(), 10) || 1;
                console.log(`Total pages detected: ${totalPages}`);
            }
        } else {
            console.log('No pagination found. Assuming single page.');
        }

        const allComments = new Map();

        // Collect comments from each page with expanded replies
        for (let page = 1; page <= totalPages; page++) {
            console.log(`Processing page ${page}...`);
            try {
                masterCommentList = await waitForElement('ol.commentList.commentList--anchored', 1);
                console.log(`Expanding replies on page ${page}...`);
                await expandAllReplies(); // Expand replies on the current page
                const comments = masterCommentList.querySelectorAll('.commentList-item');
                console.log(`Found ${comments.length} comments on page ${page} (with replies)`);
                comments.forEach(comment => {
                    const commentId = comment.getAttribute('data-id');
                    if (commentId && !allComments.has(commentId)) {
                        const clonedComment = comment.cloneNode(true);
                        const commentBody = clonedComment.querySelector('.comment-body .userHtml');
                        if (commentBody) {
                            const indicator = document.createElement('span');
                            indicator.textContent = ` [from page ${page} of ${totalPages}]`;
                            indicator.style.cssText = PAGE_INDICATOR_STYLE;
                            commentBody.appendChild(indicator);
                        }
                        allComments.set(commentId, clonedComment);
                    }
                });

                if (page < totalPages) {
                    const nextButton = pagination.querySelector('button[aria-label="Nächste Seite"]'); // This selector must match the site
                    if (!nextButton || nextButton.hasAttribute('disabled')) {
                        console.log(`No "Next Page" button available or disabled on page ${page}. Stopping.`);
                        break;
                    }
                    console.log(`Clicking "Next Page" for page ${page + 1}...`);
                    nextButton.click();
                    await new Promise(res => setTimeout(res, PAGE_TRANSITION_DELAY));
                }
            } catch (error) {
                console.error(`Error processing page ${page}:`, error);
            }
        }

        // Append all collected comments
        masterCommentList = document.querySelector('ol.commentList.commentList--anchored');
        if (masterCommentList) {
            console.log(`Before adding, the master list has ${masterCommentList.children.length} elements`);
            masterCommentList.innerHTML = '';
            allComments.forEach(comment => {
                masterCommentList.appendChild(comment);
            });
            console.log(`After adding, the master list has ${masterCommentList.children.length} elements`);
            console.log(`Appended ${allComments.size} unique comments with replies.`);
            updateButtonState(
                loadButton,
                'success',
                'Success!', // Or provide a count: `Done (${allComments.size} comments)`
                LOAD_ALL_BUTTON_STYLE,
                LOAD_ALL_PROCESSING_STYLE,
                LOAD_ALL_SUCCESS_STYLE
            );
        } else {
            console.log('Master comment list not found after processing. Cannot append comments.');
            updateButtonState(
                loadButton,
                'default',
                'Load All Comments & Expand Replies',
                LOAD_ALL_BUTTON_STYLE,
                LOAD_ALL_PROCESSING_STYLE,
                LOAD_ALL_SUCCESS_STYLE
            );
        }

        // Clear the trigger flag
        localStorage.removeItem('mydealz_script_triggered');
    }

    // Function to handle the expand replies button click
    async function handleExpandReplies() {
        const expandButton = ensureButton(
            'mydealz-expand-replies-button',
            'Expand All Replies',
            EXPAND_REPLIES_BUTTON_STYLE,
            handleExpandReplies
        );
        await expandAllReplies(expandButton);
    }

    // Initialize buttons
    function initializeButtons() {
        ensureButton(
            'mydealz-load-all-button',
            'Load All Comments & Expand Replies',
            LOAD_ALL_BUTTON_STYLE,
            loadAllCommentsAndExpandReplies
        );
        ensureButton(
            'mydealz-expand-replies-button',
            'Expand All Replies',
            EXPAND_REPLIES_BUTTON_STYLE,
            handleExpandReplies
        );
    }

    // Auto-run only if triggered from a previous page reload
    if (window.location.hash === '#comments' && localStorage.getItem('mydealz_script_triggered') === 'true') {
        console.log('Button was clicked previously. Auto-running script on #comments...');
        loadAllCommentsAndExpandReplies();
    } else if (document.readyState === 'complete') {
        initializeButtons();
    } else {
        window.addEventListener('load', () => {
            initializeButtons();
            // Ensure buttons are reset to default state if not auto-running
            const loadAllBtn = document.querySelector('.mydealz-load-all-button');
            if (loadAllBtn) {
                updateButtonState(
                    loadAllBtn,
                    'default',
                    'Load All Comments & Expand Replies',
                    LOAD_ALL_BUTTON_STYLE,
                    LOAD_ALL_PROCESSING_STYLE,
                    LOAD_ALL_SUCCESS_STYLE
                );
            }
            const expandRepliesBtn = document.querySelector('.mydealz-expand-replies-button');
            if (expandRepliesBtn) {
                updateButtonState(
                    expandRepliesBtn,
                    'default',
                    'Expand All Replies',
                    EXPAND_REPLIES_BUTTON_STYLE,
                    EXPAND_REPLIES_PROCESSING_STYLE,
                    EXPAND_REPLIES_SUCCESS_STYLE
                );
            }
        });
    }

    console.log('MyDealz Comment Enhancer script (English) initialized with two buttons.');
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。