// ==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.');
})();