// ==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 // @downloadURL 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 => `${match}` ); } // 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. `; 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 = '
'; html += `

Es wurden ${totalComments} Kommentare durchsucht und ${filteredComments.length} Kommentare mit '${searchTerm}' gefunden.

`; filteredComments.forEach(item => { const dealId = getDealId(); const url = `https://www.mydealz.de/${dealId}#${item.type}-${item.commentId}`; html += `
đź”— ${item.createdAt} ${item.user.username} : ${highlightSearchTerm( outputType === 'compact' ? cleanHTML(item.preparedHtmlContent) : item.preparedHtmlContent, searchTerm )}
`; }); html += '
'; 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(` Kommentar-Suche ${createSearchForm(title, searchTerm)} `); newWindow.document.close(); newWindow.searchComments = searchComments; newWindow.reloadFromServer = reloadFromServer; attachEventListeners(); newWindow.searchComments(); } function createSearchForm(title, searchTerm) { return `
Darstellung Sortierung
`; } // 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); } } })();