// ==UserScript==
// @name MyDealz Kommentarvolltextsuche
// @namespace https://mydealz.de/
// @version 1.0
// @description Suchbox für Volltextsuche in allen Kommentaren eines Deals / einer Diskussion
// @match https://www.mydealz.de/deals/*
// @match https://www.mydealz.de/diskussion/*
// @match https://www.mydealz.de/feedback/*
// @license MIT
// @grant none
// ==/UserScript==
(function() {
'use strict';
let newWindow;
const API_URL = 'https://www.mydealz.de/graphql';
// Basis-Funktionen
function getDealId() {
const match = window.location.href.match(/-(\d+)(?=[/?#]|$)/);
return match ? match[1] : null;
}
function extractThreadId() {
const mainElement = document.getElementById('main');
if (!mainElement) return null;
const dataAttribute = mainElement.getAttribute('data-t-d');
if (!dataAttribute) return null;
return JSON.parse(dataAttribute.replace(/"/g, '"')).threadId;
}
function cleanHTML(html) {
return html.replace(/<.*?>/g, '');
}
function highlightSearchTerm(text, searchTerm) {
return text.replace(
new RegExp(searchTerm, 'gi'),
match => `<b style="color:#4CAF50;">${match}</b>`
);
}
// GraphQL-Funktionen
async function fetchGraphQLData(query, variables) {
const response = await fetch(API_URL, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query, variables})
});
if (response.status === 429) {
await new Promise(resolve => setTimeout(resolve, 10000));
return fetchGraphQLData(query, variables);
}
const data = await response.json();
if (data.errors) throw new Error(data.errors[0].message);
return data.data.comments;
}
async function fetchAllPages(query, variables) {
let currentPage = 1;
let allData = [];
while (true) {
const data = await fetchGraphQLData(query, {...variables, page: currentPage});
allData.push(...data.items);
if (!data.pagination.next) break;
currentPage++;
}
return allData;
}
async function fetchAllComments() {
let allComments = [];
let currentPage = 1;
let hasMorePages = true;
const threadId = extractThreadId();
while (hasMorePages) {
const query = `
query comments($filter: CommentFilter!, $limit: Int, $page: Int) {
comments(filter: $filter, limit: $limit, page: $page) {
items { commentId replyCount }
pagination { current next }
}
}
`;
const variables = {
filter: { threadId: {eq: threadId} },
limit: 100,
page: currentPage
};
const response = await fetch(API_URL, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query, variables})
});
const data = await response.json();
if (data.errors) throw new Error(data.errors[0].message);
allComments = allComments.concat(data.data.comments.items);
hasMorePages = !!data.data.comments.pagination.next;
if (hasMorePages) currentPage++;
}
return allComments;
}
async function fetchReplies(commentId, threadId) {
const query = `
query comments($filter: CommentFilter!, $limit: Int, $page: Int) {
comments(filter: $filter, limit: $limit, page: $page) {
items {
commentId preparedHtmlContent user { userId username }
replyCount createdAt parentReply { user { username } }
}
pagination { current next }
}
}
`;
return await fetchAllPages(query, {
filter: {
mainCommentId: commentId,
threadId: {eq: threadId},
order: {direction: "Ascending"}
},
limit: 100
});
}
async function fetchDataAndReplies(forceReload = false) {
const dealId = getDealId();
const threadId = extractThreadId();
const savedComments = JSON.parse(localStorage.getItem('dealComments_' + dealId)) || [];
if (!forceReload && savedComments.length > 0) {
const allComments = await fetchAllComments();
let totalReplies = 0;
allComments.forEach(comment => {
totalReplies += comment.replyCount || 0;
});
const onlineCommentCount = allComments.length + totalReplies;
const localCommentCount = savedComments.reduce((acc, comment) =>
acc + 1 + (comment.replies?.length || 0), 0);
if (localCommentCount < onlineCommentCount) {
const newCommentCount = onlineCommentCount - localCommentCount;
newWindow.document.getElementById('newCommentsStatus').innerHTML =
`Es sind ${newCommentCount} neue Kommentare vorhanden.
<button onclick="reloadFromServer()"
style="background-color:#4CAF50;color:white;padding:5px 10px;
border:none;border-radius:5px;font-size:14px;cursor:pointer;
box-shadow:0 2px 4px rgba(0,0,0,0.2);">
Neue Kommentare laden
</button>`;
return savedComments;
}
return savedComments;
}
const query = `
query comments($filter: CommentFilter!, $limit: Int, $page: Int) {
comments(filter: $filter, limit: $limit, page: $page) {
items {
commentId preparedHtmlContent user { userId username }
replyCount createdAt
}
pagination { current next }
}
}
`;
newWindow.document.getElementById('progressBar').style.display = 'block';
let allData = await fetchAllPages(query, {
filter: {
threadId: {eq: threadId},
order: {direction: "Ascending"}
},
limit: 100
});
let totalItems = allData.length + allData.reduce((acc, c) => acc + (c.replyCount || 0), 0);
let processedItems = 0;
for (const comment of allData) {
processedItems++;
updateProgress(processedItems, totalItems);
if (comment.replyCount > 0) {
const replies = await fetchReplies(comment.commentId, threadId);
comment.replies = replies;
processedItems += replies.length;
updateProgress(processedItems, totalItems);
}
}
localStorage.setItem('dealComments_' + dealId, JSON.stringify(allData));
localStorage.setItem('dealComments_' + dealId + '_timestamp', new Date().toISOString());
return allData;
}
function updateProgress(processed, total) {
const percentage = Math.round((processed / total) * 100);
newWindow.document.getElementById('progress').innerText =
processed === total ? 'Alle Kommentare durchsucht' :
`Fortschritt: ${percentage}%`;
newWindow.document.getElementById('progressBarFill').style.width = `${percentage}%`;
}
function processComments(allData, searchTerm) {
const outputType = newWindow.document.querySelector('input[name="outputType"]:checked').value;
const sortType = newWindow.document.querySelector('input[name="sortType"]:checked').value;
const filteredComments = [];
let totalComments = allData.length;
allData.forEach(comment => {
if (comment.preparedHtmlContent.toLowerCase().includes(searchTerm.toLowerCase())) {
filteredComments.push({...comment, type: 'comment'});
}
if (comment.replies) {
comment.replies.forEach(reply => {
totalComments++;
if (reply.preparedHtmlContent.toLowerCase().includes(searchTerm.toLowerCase())) {
filteredComments.push({...reply, type: 'reply'});
}
});
}
});
filteredComments.sort((a, b) => {
return sortType === 'newest' ? b.commentId - a.commentId : a.commentId - b.commentId;
});
let html = '<div class="comments-container">';
html += `<p>Es wurden ${totalComments} Kommentare durchsucht und
${filteredComments.length} Kommentare mit '${searchTerm}' gefunden.</p>`;
filteredComments.forEach(item => {
const dealId = getDealId();
const url = `https://www.mydealz.de/${dealId}#${item.type}-${item.commentId}`;
html += `
<div class="${item.type}"
style="padding:10px;margin-bottom:10px;background-color:white;
border-radius:5px;box-shadow:0 2px 4px rgba(0,0,0,0.1);">
<a href="${url}" target="_blank">
🔗 ${item.createdAt} ${item.user.username}
</a>:
${highlightSearchTerm(
outputType === 'compact' ?
cleanHTML(item.preparedHtmlContent) :
item.preparedHtmlContent,
searchTerm
)}
</div>`;
});
html += '</div>';
newWindow.document.getElementById('results').innerHTML = html;
newWindow.document.getElementById('progressBar').style.display = 'none';
}
function searchComments(forceReload = false) {
const searchTerm = newWindow.document.getElementById('searchTerm').value;
if (!searchTerm) {
alert("Kein Suchbegriff eingegeben.");
return;
}
newWindow.document.getElementById('results').innerHTML = 'Suche läuft...';
newWindow.document.getElementById('progressBar').style.display = 'block';
fetchDataAndReplies(forceReload)
.then(allData => {
processComments(allData, searchTerm);
})
.catch(error => {
console.error('Error:', error);
newWindow.document.getElementById('results').innerHTML =
'Fehler bei der Suche: ' + error.message;
});
}
function reloadFromServer() {
const dealId = getDealId();
localStorage.removeItem('dealComments_' + dealId);
localStorage.removeItem('dealComments_' + dealId + '_timestamp');
searchComments(true);
}
function attachEventListeners() {
newWindow.document.querySelectorAll('input[name="outputType"], input[name="sortType"]')
.forEach(radio => {
radio.addEventListener('change', () => {
searchComments();
});
});
}
function handleSearch(searchInput) {
const searchTerm = searchInput.value.trim();
if (!searchTerm || searchTerm === searchInput.placeholder) return;
const title = document.title.replace(" | mydealz", "");
newWindow = window.open('', '_blank');
newWindow.document.write(`
<html>
<head>
<title>Kommentar-Suche</title>
<style>
body { margin: 0; padding: 0; background-color: #f9f9f9; }
#header {
background-color: #005293;
height: 56px;
display: flex;
align-items: center;
width: 100%;
color: white;
}
#header img { height: 40px; margin-left: 20px; }
#header h2 {
margin-left: auto;
margin-right: auto;
font-size: x-large;
}
#results { margin-top: 20px; padding: 20px; }
h2, input, button, label {
font-family: sans-serif;
font-size: 14px;
}
input[type=text] { border-radius: 5px; border-width: 1px; }
button:hover { background-color: #45a049; }
#progress {
text-align: center;
margin-top: 15px;
font-size: .9em;
color: #555;
}
#progressBarFill:hover { opacity: .8; }
</style>
</head>
<body>
<div id="header">
<img src="https://www.mydealz.de/assets/img/logo/default-light_d4b86.svg"
alt="mydealz logo">
<h2>Kommentarvolltextsuche</h2>
</div>
${createSearchForm(title, searchTerm)}
</body>
</html>
`);
newWindow.document.close();
newWindow.searchComments = searchComments;
newWindow.reloadFromServer = reloadFromServer;
attachEventListeners();
newWindow.searchComments();
}
function createSearchForm(title, searchTerm) {
return `
<div style="padding:20px;text-align:center;">
<form id="searchForm" onsubmit="searchComments(); return false;"
style="display:flex;justify-content:center;align-items:center;gap:10px;">
<input type="text" id="searchTerm" placeholder="Suchbegriff eingeben"
value="${searchTerm}"
style="width:50%;padding:10px;border-radius:5px;border:1px solid #ccc;">
<button type="submit"
style="background-color:#4CAF50;color:white;padding:10px 20px;
border:none;border-radius:5px;font-size:16px;cursor:pointer;
box-shadow:0 2px 4px rgba(0,0,0,0.2);display:flex;
align-items:center;">
<span style="margin-right:8px;">▶</span>Suchen
</button>
</form>
<div id="options" style="text-align:left;margin-top:10px;margin-left:25%;
font-size:14px;">
<div>
Darstellung
<input type="radio" id="compact" name="outputType" value="compact" checked>
<label for="compact"> kompakt</label>
<input type="radio" id="detailed" name="outputType" value="detailed"
style="margin-left:10px;">
<label for="detailed"> ausführlich</label>
<span style="margin-left:20px;">
Sortierung
<input type="radio" id="sortNewest" name="sortType" value="newest"
checked>
<label for="sortNewest"> neueste zuerst</label>
<input type="radio" id="sortOldest" name="sortType" value="oldest"
style="margin-left:10px;">
<label for="sortOldest"> älteste zuerst</label>
</span>
</div>
<div id="newCommentsStatus" style="margin-top:10px;"></div>
</div>
</div>
<div id="results" style="width:90%;margin:20px auto;background-color:#f9f9f9;
padding:20px;border-radius:5px;"></div>
<div id="progress" style="text-align:center;margin-top:20px;font-size:14px;"></div>
<div id="progressBar" style="width:75%;margin:20px auto;background-color:#e0e0e0;
height:20px;border-radius:10px;overflow:hidden;">
<div id="progressBarFill" style="width:0%;height:100%;background-color:#4CAF50;
transition:width 0.3s;"></div>
</div>
`;
}
// Initialisierung
const observer = new MutationObserver((mutations, obs) => {
if (document.querySelector('.comment-search-container')) return;
const sortLabel = document.querySelector('.size--all-m.size--fromW3-l.overflow--wrap-off');
if (sortLabel && sortLabel.textContent.includes('sortiert nach')) {
injectSearchBox(sortLabel);
obs.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
function injectSearchBox(targetElement) {
const searchContainer = document.createElement('div');
searchContainer.className = 'comment-search-container';
searchContainer.style.cssText = `
display: inline-flex;
align-items: center;
margin-left: 15px;
margin-right: 15px;
flex: 0 1 auto;
`;
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = 'In allen Kommentaren suchen';
searchInput.style.cssText = `
width: 240px;
padding: 4px 8px;
border: 1px solid #e0e0e0;
border-radius: 4px;
font-size: 14px;
height: 28px;
color: #666;
background-color: white;
`;
const searchButton = document.createElement('button');
searchButton.textContent = 'Suchen';
searchButton.style.cssText = `
margin-left: 8px;
padding: 4px 12px;
border: none;
border-radius: 4px;
background-color: #4CAF50;
color: white;
cursor: pointer;
`;
searchInput.addEventListener('focus', () => {
if (searchInput.value === searchInput.placeholder) {
searchInput.value = '';
searchInput.style.color = '#000';
}
});
searchInput.addEventListener('blur', () => {
if (!searchInput.value.trim()) {
searchInput.value = searchInput.placeholder;
searchInput.style.color = '#666';
}
});
searchButton.addEventListener('click', () => handleSearch(searchInput));
searchInput.addEventListener('keydown', e => {
if (e.key === 'Enter') handleSearch(searchInput);
});
searchContainer.appendChild(searchInput);
searchContainer.appendChild(searchButton);
const bellIcon = targetElement.parentNode.querySelector('button[title="Folgen"]');
if (bellIcon) {
bellIcon.parentNode.insertBefore(searchContainer, bellIcon);
} else {
targetElement.parentNode.appendChild(searchContainer);
}
}
})();