您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Suchbox für Volltextsuche in allen Kommentaren eines Deals / einer Diskussion
// ==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); } } })();