您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Deals gezielt ausblenden mittels X Button, Filtern nach Händlern und Wörtern im Titel. Teure und kalte Deals ausblenden.
当前为
// ==UserScript== // @name mydealz Manager // @namespace http://tampermonkey.net/ // @version 1.12.7 // @description Deals gezielt ausblenden mittels X Button, Filtern nach Händlern und Wörtern im Titel. Teure und kalte Deals ausblenden. // @author Moritz Baumeister (https://www.mydealz.de/profile/BobBaumeister) (https://github.com/grapefruit89) & Flo (https://www.mydealz.de/profile/Basics0119) (https://github.com/9jS2PL5T) // @license MIT // @match https://www.mydealz.de/* // @match https://www.preisjaeger.at/* // @icon https://www.google.com/s2/favicons?sz=64&domain=mydealz.de // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // ==/UserScript== // Versions-Änderungen // FIX: Maximalpreis - Deals mit Preis >999,99 wurden nicht gefiltert, wenn Maximalpreis niedriger war. Komma wurde nicht berücksichtigt (Bsp 999,99 -> 999999). // FIX: Der Wortfilter hat Groß- und Kleinschreibung unterschieden, wodurch doppelte Einträge entstanden sind (z. B. 'geschirrspüler' und 'Geschirrspüler'). // FIX: Gespeicherte Daten wurden bei einem Script Update nicht übernommen, da sie seit 1.12.5 nur noch im GM Storage lagen. Von dort ließen sie sich aber nicht in eine neuere Version übernehmen. Daher werden Daten nun auch wieder im localstorage gespeichert. // CHANGE: Backup - maxprice und hidecolddeals werden nun auch im Backup gesichert und wiederhergestellt. // CHANGE: Design an mydealz Designänderung angepasst. // REMOVE: Nicht benötigter Code wurde entfernt. // Einbinden von Font Awesome für Icons const fontAwesomeLink = document.createElement('link'); fontAwesomeLink.rel = 'stylesheet'; fontAwesomeLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css'; document.head.appendChild(fontAwesomeLink); // Add constant for touch detection const IS_TOUCH_DEVICE = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0); // Dark mode color constants const THEME_COLORS = { light: { background: '#f9f9f9', border: '#ccc', text: '#333', buttonBg: '#f0f0f0', buttonBorder: '#ccc', inputBg: '#fff', itemBg: '#f0f0f0', itemHoverBg: '#e8e8e8' }, dark: { background: '#1f1f1f', border: '#2d2d2d', text: '#ffffff', buttonBg: '#2d2d2d', buttonBorder: '#3d3d3d', inputBg: '#2d2d2d', itemBg: '#2d2d2d', itemHoverBg: '#3d3d3d' } }; const themeObserver = new MutationObserver((mutations) => { requestAnimationFrame(() => { const isLight = !isDarkMode(); updateAllUIThemes(isLight); }); }); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] }); themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-theme'] }); // --- 1. Initial Setup --- const EXCLUDE_WORDS_KEY = 'excludeWords'; const EXCLUDE_MERCHANTS_KEY = 'excludeMerchantIDs'; const HIDDEN_DEALS_KEY = 'hiddenDeals'; const MERCHANT_PAGE_SELECTOR = '.merchant-banner'; const HIDE_COLD_DEALS_KEY = 'hideColdDeals'; const MAX_PRICE_KEY = 'maxPrice'; // Load data immediately let excludeWords = []; let excludeMerchantIDs = []; let hiddenDeals = []; let suggestedWords = []; let activeSubUI = null; let dealThatOpenedSettings = null; let settingsDiv = null; let merchantListDiv = null; let wordsListDiv = null; let uiClickOutsideHandler = null; let isSettingsOpen = false; let hideColdDeals = localStorage.getItem(HIDE_COLD_DEALS_KEY) === 'true'; let maxPrice = parseFloat(localStorage.getItem(MAX_PRICE_KEY)) || 0; let suggestionClickHandler = null; // Sync-Funktion für beide Storage-Systeme async function syncStorage() { // Prüfe ob Migration bereits durchgeführt wurde const migrationComplete = GM_getValue('migrationComplete', false); if (migrationComplete) { return; } // Prüfe ob GM_Storage Daten hat const gmExcludeWords = GM_getValue('excludeWords', null); const gmExcludeMerchants = GM_getValue('excludeMerchantsData', null); const gmHiddenDeals = GM_getValue('hiddenDeals', null); const gmHideColdDeals = GM_getValue('hideColdDeals', null); const gmMaxPrice = GM_getValue('maxPrice', null); // Prüfe ob localStorage Daten hat const lsExcludeWords = JSON.parse(localStorage.getItem('excludeWords') || 'null'); const lsExcludeMerchants = JSON.parse(localStorage.getItem('excludeMerchantsData') || 'null'); const lsHiddenDeals = JSON.parse(localStorage.getItem('hiddenDeals') || 'null'); const lsHideColdDeals = localStorage.getItem('hideColdDeals') || 'null'; const lsMaxPrice = localStorage.getItem('maxPrice') || 'null'; // Migration von localStorage zu GM_storage wenn nötig let migrationPerformed = false; if (!gmExcludeWords && lsExcludeWords) { GM_setValue('excludeWords', lsExcludeWords); excludeWords = lsExcludeWords; migrationPerformed = true; } if (!gmExcludeMerchants && lsExcludeMerchants) { GM_setValue('excludeMerchantsData', lsExcludeMerchants); excludeMerchantIDs = lsExcludeMerchants.map(m => m.id); migrationPerformed = true; } if (!gmHiddenDeals && lsHiddenDeals) { GM_setValue('hiddenDeals', lsHiddenDeals); hiddenDeals = lsHiddenDeals; migrationPerformed = true; } if (!gmHideColdDeals && lsHideColdDeals !== 'null') { GM_setValue('hideColdDeals', lsHideColdDeals === 'true'); hideColdDeals = lsHideColdDeals === 'true'; migrationPerformed = true; } if (!gmMaxPrice && lsMaxPrice !== 'null') { GM_setValue('maxPrice', lsMaxPrice); maxPrice = parseFloat(lsMaxPrice); migrationPerformed = true; } if (migrationPerformed) { GM_setValue('migrationComplete', true); } // Synchronisiere localStorage mit GM_Storage localStorage.setItem('excludeWords', JSON.stringify(gmExcludeWords || [])); localStorage.setItem('excludeMerchantsData', JSON.stringify(gmExcludeMerchants || [])); localStorage.setItem('hiddenDeals', JSON.stringify(gmHiddenDeals || [])); localStorage.setItem('hideColdDeals', (gmHideColdDeals || false).toString()); localStorage.setItem('maxPrice', (gmMaxPrice || '0').toString()); } function updateAllUIThemes(isLight) { // Update buttons document.querySelectorAll('.custom-hide-button').forEach(button => { if (button) { const bgColor = isLight ? '#ffffff' : '#1d1f20'; button.style.setProperty('background', bgColor, 'important'); } }); // Update open UIs if (isSettingsOpen || activeSubUI) { updateUITheme(); } // Update filter menu if open const filterMenu = document.querySelector('.subNavMenu-list'); if (filterMenu) { const colors = getThemeColors(); const inputs = filterMenu.querySelectorAll('input'); inputs.forEach(input => { input.style.borderColor = colors.border; input.style.backgroundColor = colors.inputBg; input.style.color = colors.text; }); } } function isDarkMode() { // Check system preference const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; // Check document theme const htmlElement = document.documentElement; const bodyElement = document.body; // Check for dark theme indicators const isDark = htmlElement.classList.contains('dark') || bodyElement.classList.contains('dark') || htmlElement.getAttribute('data-theme') === 'dark' || document.querySelector('html[data-theme="dark"]') !== null || (prefersDark && !htmlElement.classList.contains('light')); // System dark + no explicit light return isDark; } const systemThemeObserver = window.matchMedia('(prefers-color-scheme: dark)'); systemThemeObserver.addListener((e) => { requestAnimationFrame(() => { const isLight = !isDarkMode(); updateAllUIThemes(isLight); }); }); const hideButtonThemeObserver = new MutationObserver(() => { const isLight = !isDarkMode(); requestAnimationFrame(() => { document.querySelectorAll('.custom-hide-button').forEach(button => { if (button) { const bgColor = isLight ? '#ffffff' : '#1d1f20'; const borderColor = isLight ? 'rgba(3,12,25,0.23)' : 'rgb(107, 109, 109)'; button.style.cssText = ` position: absolute !important; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; z-index: 10002 !important; background: ${bgColor} !important; border: 1px solid ${borderColor} !important; border-radius: 50% !important; cursor: pointer !important; padding: 4px !important; width: 28px !important; height: 28px !important; display: flex !important; align-items: center !important; justify-content: center !important; pointer-events: all !important; box-shadow: none !important; font-size: 12px !important; `; } }); // Update settings UI wenn offen if (isSettingsOpen) { updateUITheme(); } }); }); hideButtonThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); function initUIContainers() { settingsDiv = document.createElement('div'); merchantListDiv = document.createElement('div'); wordsListDiv = document.createElement('div'); } // --- 1. Core Functions --- function processArticles() { // Cache für bereits verarbeitete Artikel const processedDeals = new Set(); const deals = document.querySelectorAll('article.thread--deal, article.thread--voucher'); deals.forEach(deal => { const dealId = deal.getAttribute('id'); // Skip wenn bereits verarbeitet if (processedDeals.has(dealId)) return; processedDeals.add(dealId); if (hiddenDeals.includes(dealId)) { hideDeal(deal); return; } if (shouldExcludeArticle(deal)) { hideDeal(deal); return; } deal.style.display = 'block'; deal.style.opacity = '1'; }); } function getThemeColors() { const isDark = isDarkMode(); return isDark ? THEME_COLORS.dark : THEME_COLORS.light; } // Update word/merchant item styles in list creation function updateItemStyles(item, colors) { item.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: ${colors.itemBg}; color: ${colors.text}; border: 1px solid ${colors.border}; border-radius: 3px; `; } // Update createMerchantListUI and createExcludeWordsUI function updateListStyles(listDiv, colors) { // Apply styles to list items listDiv.querySelectorAll('.merchant-item, .word-item').forEach(item => { updateItemStyles(item, colors); }); // Update search input const searchInput = listDiv.querySelector('input[type="text"]'); if (searchInput) { searchInput.style.cssText = ` width: 100%; padding: 5px; margin-bottom: 10px; background: ${colors.inputBg}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 3px; `; } // Update clear button const clearButton = listDiv.querySelector('[id*="clear"]'); if (clearButton) { clearButton.style.cssText = ` width: 100%; padding: 5px 10px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; color: ${colors.text}; border-radius: 3px; cursor: pointer; margin-top: 10px; `; } } function updateUITheme() { const colors = getThemeColors(); [settingsDiv, merchantListDiv, wordsListDiv].forEach(div => { if (div?.parentNode) { div.style.background = colors.background; div.style.border = `1px solid ${colors.border}`; div.style.color = colors.text; // Update all buttons and inputs div.querySelectorAll('button:not([id*="close"])').forEach(btn => { btn.style.background = colors.buttonBg; btn.style.border = `1px solid ${colors.buttonBorder}`; btn.style.color = colors.text; }); div.querySelectorAll('input').forEach(input => { input.style.background = colors.inputBg; input.style.border = `1px solid ${colors.border}`; input.style.color = colors.text; }); } }); } // Define observer const observer = new MutationObserver(throttle((mutations) => { processArticles(); addSettingsButton(); addHideButtons(); }, 250)); function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => { inThrottle = false; return false; }, limit); } } } // Initialize everything (function init() { syncStorage(); processArticles(); addSettingsButton(); addHideButtons(); observer.observe(document.body, { childList: true, subtree: true }); })(); // --- 2. Hilfsfunktionen --- function shouldExcludeArticle(article) { const titleElement = article.querySelector('.thread-title'); if (!titleElement) return false; // 2. Quick checks (temperature & price) // Temperature check if (hideColdDeals) { const tempElement = article.querySelector('.cept-vote-temp .overflow--wrap-off'); if (tempElement) { const temp = parseInt(tempElement.textContent); if (!isNaN(temp) && temp < 0) return true; } } // Price check if (maxPrice > 0) { const priceSelectors = ['.threadItemCard-price', '.thread-price', '[class*="price"]', '.cept-tp']; for (const selector of priceSelectors) { const priceElement = article.querySelector(selector); if (!priceElement) continue; try { const priceMatch = priceElement.textContent.trim().match(/(\d+(?:[,.]\d{1,2})?)\s*€/); if (priceMatch) { const price = parseFloat(priceMatch[1].replace(',', '.')); if (!isNaN(price) && price > maxPrice) return true; } } catch (error) { continue; } } } // 3. Complex checks // Word check const normalizedTitle = decodeHtml(titleElement.innerHTML) .toLowerCase() .replace(/ /g, ' ') .replace(/[(){}»«„"\/\[\]]/g, ' ') // Don't replace hyphens .replace(/,/g, ' ') // Kommas durch Leerzeichen ersetzen .replace(/\s+/g, ' ') .trim(); if (excludeWords.some(word => { const searchTerm = word.toLowerCase().trim(); // If searchTerm contains hyphens, look for it as part of hyphenated words if (searchTerm.includes('-')) { // Split title into hyphenated word groups const hyphenatedWords = normalizedTitle.match(/\S+(?:-\S+)*/g) || []; return hyphenatedWords.some(titleWord => // Check if any hyphenated word contains our search term titleWord.includes(searchTerm) ); } else { // For non-hyphenated words, require exact matches return normalizedTitle.split(/\s+/).some(titleWord => titleWord === searchTerm || // Exact match (searchTerm.includes(' ') && normalizedTitle.includes(searchTerm)) // Multi-word terms ); } })) { return true; } // Merchant check const merchantLink = article.querySelector('a[href*="merchant-id="]'); if (merchantLink) { const merchantIDMatch = merchantLink.getAttribute('href').match(/merchant-id=(\d+)/); if (merchantIDMatch && excludeMerchantIDs.includes(merchantIDMatch[1])) { return true; } } return false; } function hideDeal(deal) { deal.style.display = 'none'; } // Funktion zum Speichern der ausgeblendeten Deals function saveHiddenDeals() { GM_setValue('hiddenDeals', hiddenDeals); localStorage.setItem('hiddenDeals', JSON.stringify(hiddenDeals)); } // Speichern der `excludeWords` und `excludeMerchantIDs` function saveExcludeWords(words) { // Normalisiere nur Groß-/Kleinschreibung und entferne Duplikate const normalizedWords = words.reduce((acc, word) => { // Konvertiere zum Vergleich in Kleinbuchstaben const lowerWord = word.toLowerCase(); // Prüfe ob das Wort (unabhängig von Groß-/Kleinschreibung) bereits existiert const exists = acc.some(w => w.toLowerCase() === lowerWord); if (!exists) { // Wenn noch nicht vorhanden, füge das originale Wort hinzu acc.push(word); } return acc; }, []); // Speichere in beiden Systemen GM_setValue('excludeWords', normalizedWords); localStorage.setItem('excludeWords', JSON.stringify(normalizedWords)); } function loadExcludeWords() { // Versuche zuerst GM Storage const gmWords = GM_getValue('excludeWords', []); // Dann localStorage let lsWords = []; try { lsWords = JSON.parse(localStorage.getItem('excludeWords') || '[]'); } catch (e) { console.error('Error loading words from localStorage:', e); } // Kombiniere beide, GM hat Priorität return gmWords.length > 0 ? gmWords : lsWords; } function saveExcludeMerchants(merchantsData) { const validMerchants = merchantsData.filter(m => m && typeof m.id !== 'undefined' && m.id !== null && typeof m.name !== 'undefined' && m.name !== null ); const ids = validMerchants.map(m => m.id); GM_setValue('excludeMerchantIDs', ids); GM_setValue('excludeMerchantsData', validMerchants); localStorage.setItem('excludeMerchantsData', JSON.stringify(validMerchants)); excludeMerchantIDs = ids; } function loadExcludeMerchants() { const merchantsData = GM_getValue('excludeMerchantsData', []); const legacyIds = GM_getValue('excludeMerchantIDs', []); // Filter out invalid entries const validMerchants = merchantsData.filter(m => m && typeof m.id !== 'undefined' && m.id !== null && typeof m.name !== 'undefined' && m.name !== null ); // Convert legacy IDs if needed if (validMerchants.length === 0 && legacyIds.length > 0) { return legacyIds .filter(id => id && typeof id !== 'undefined') .map(id => ({ id, name: id })); } return validMerchants; } // Clean up existing data on script init (function cleanupMerchantData() { const merchants = loadExcludeMerchants(); saveExcludeMerchants(merchants); })(); // Fügt Event Listener hinzu, um Auto-Speichern zu ermöglichen function addAutoSaveListeners() { // Event Listener für Eingabefelder const excludeWordsInput = document.getElementById('excludeWordsInput'); excludeWordsInput.addEventListener('input', () => { const newWords = excludeWordsInput.value.split('\n').map(w => w.trim()).filter(Boolean); saveExcludeWords(newWords); excludeWords = newWords; processArticles(); }); const excludeMerchantIDsInput = document.getElementById('excludeMerchantIDsInput'); excludeMerchantIDsInput.addEventListener('input', () => { const newMerchantIDs = excludeMerchantIDsInput.value.split('\n').map(id => id.trim()).filter(Boolean); saveExcludeMerchants(newMerchantIDs); excludeMerchantIDs = newMerchantIDs; processArticles(); }); } function addSettingsButton() { const deals = document.querySelectorAll('article.thread--deal, article.thread--voucher'); deals.forEach(deal => { if (deal.hasAttribute('data-settings-added')) return; const footer = deal.querySelector('.threadListCard-footer, .threadCardLayout-footer'); if (!footer) return; // Create settings button const settingsBtn = document.createElement('button'); settingsBtn.className = 'flex--shrink-0 button button--type-text button--mode-secondary button--square'; settingsBtn.title = 'mydealz Manager Einstellungen'; settingsBtn.setAttribute('data-t', 'mdmSettings'); settingsBtn.style.cssText = ` display: inline-flex !important; align-items: center !important; justify-content: center !important; padding: 6px !important; border: none !important; background: transparent !important; cursor: pointer !important; margin: 0 4px !important; min-width: 32px !important; min-height: 32px !important; position: relative !important; z-index: 2 !important; `; settingsBtn.innerHTML = ` <span class="flex--inline boxAlign-ai--all-c"> <svg width="20" height="20" class="icon icon--gear"> <use xlink:href="/assets/img/ico_707ed.svg#gear"></use> </svg> </span> `; // Insert at correct position (before comments button) const commentsBtn = footer.querySelector('[href*="comments"]'); if (commentsBtn) { commentsBtn.parentNode.insertBefore(settingsBtn, commentsBtn); } else { footer.prepend(settingsBtn); } deal.setAttribute('data-settings-added', 'true'); settingsBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); if (isSettingsOpen) { if (dealThatOpenedSettings === deal) { cleanup(); } else { // Komplett neues UI erstellen statt nur den Button zu aktualisieren cleanup(); dealThatOpenedSettings = deal; createSettingsUI(); // Dies erstellt das UI in der korrekten Reihenfolge } } else { dealThatOpenedSettings = deal; createSettingsUI(); } return false; }; }); } document.addEventListener('DOMContentLoaded', () => { // Bestehende Funktionen processArticles(); addSettingsButton(); addMerchantPageHideButton(); initObserver(); injectMaxPriceFilter(); // Neue Theme-Erkennung const isLight = !isDarkMode(); // Theme Observer starten hideButtonThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); // Initial Theme auf Buttons anwenden document.querySelectorAll('.custom-hide-button').forEach(button => { if (button) { const bgColor = isLight ? '#f2f5f7' : '#1d1f20'; button.style.setProperty('background', bgColor, 'important'); button.style.setProperty('border-radius', '50%', 'important'); } }); }); function addHideButtons() { const deals = document.querySelectorAll('article:not([data-button-added])'); deals.forEach(deal => { if (deal.hasAttribute('data-button-added')) return; // Check for expired status const isExpired = deal.querySelector('.color--text-TranslucentSecondary .size--all-s')?.textContent.includes('Abgelaufen'); // Get temperature container const voteTemp = deal.querySelector('.cept-vote-temp'); if (!voteTemp) return; // Remove popover const popover = voteTemp.querySelector('.popover-origin'); if (popover) popover.remove(); // Find temperature span for expired deals const tempSpan = isExpired ? voteTemp.querySelector('span') : null; const targetElement = isExpired ? tempSpan : voteTemp; if (!targetElement) return; const hideButtonContainer = document.createElement('div'); hideButtonContainer.style.cssText = ` position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: none; z-index: 10001; pointer-events: none; `; const hideButton = document.createElement('button'); hideButton.innerHTML = '❌'; hideButton.className = 'vote-button overflow--visible custom-hide-button'; hideButton.title = 'Deal verbergen'; hideButton.style.cssText = ` position: absolute !important; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; z-index: 10002 !important; background: ${isDarkMode() ? '#1d1f20' : '#ffffff'} !important; border: 1px solid ${isDarkMode() ? 'rgb(107, 109, 109)' : 'rgba(3,12,25,0.23)'} !important; border-radius: 50% !important; cursor: pointer !important; padding: 4px !important; width: 28px !important; height: 28px !important; display: flex !important; align-items: center !important; justify-content: center !important; pointer-events: all !important; box-shadow: none !important; font-size: 12px !important; `; // Position relative to container if (!targetElement.style.position) { targetElement.style.position = 'relative'; } if (IS_TOUCH_DEVICE) { let buttonVisible = false; const dealId = deal.getAttribute('id'); // Add scroll handler to hide button const scrollHandler = () => { if (buttonVisible) { buttonVisible = false; hideButtonContainer.style.display = 'none'; } else if (hideButtonContainer.style.display === 'block') { } }; // Add scroll listener window.addEventListener('scroll', scrollHandler, { passive: true }); targetElement.addEventListener('touchstart', (e) => { e.preventDefault(); e.stopPropagation(); if (!buttonVisible) { buttonVisible = true; hideButtonContainer.style.display = 'block'; } else { hiddenDeals.push(dealId); saveHiddenDeals(); hideDeal(deal); window.removeEventListener('scroll', scrollHandler); } }, true); targetElement.addEventListener('touchend', () => { if (!buttonVisible) { hideButtonContainer.style.display = 'none'; } }, true); } else { targetElement.addEventListener('mouseenter', () => { hideButtonContainer.style.display = 'block'; }, true); targetElement.addEventListener('mouseleave', () => { hideButtonContainer.style.display = 'none'; }, true); hideButton.onclick = (e) => { e.preventDefault(); e.stopPropagation(); const dealId = deal.getAttribute('id'); hiddenDeals.push(dealId); saveHiddenDeals(); hideDeal(deal); return false; }; } hideButtonContainer.appendChild(hideButton); targetElement.appendChild(hideButtonContainer); deal.setAttribute('data-button-added', 'true'); }); } // Verbesserte HTML Decoder Funktion function decodeHtml(html) { const txt = document.createElement('textarea'); txt.innerHTML = html; return txt.value; } // --- 3. Backup- und Restore-Funktionen --- function backupData() { try { // Aktuelle Daten neu laden const currentWords = loadExcludeWords(); const currentMerchants = loadExcludeMerchants(); // Backup mit aktuellen Daten erstellen const backup = { excludeWords: currentWords, merchantsData: currentMerchants, // Nur merchantsData speichern maxPrice: maxPrice, hideColdDeals: hideColdDeals }; const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); a.href = url; a.download = `mydealz_backup_${timestamp}.json`; a.click(); URL.revokeObjectURL(url); } catch (error) { console.error('Backup failed:', error); alert('Fehler beim Erstellen des Backups: ' + error.message); } } // Restore-Funktion function restoreData(event) { const file = event.target.files[0]; if (!file || file.type !== 'application/json') { alert('Bitte wählen Sie eine gültige JSON-Datei aus.'); return; } const reader = new FileReader(); reader.onload = function(e) { try { const restoredData = JSON.parse(e.target.result); // Validiere Backup-Daten if (!restoredData.excludeWords || !Array.isArray(restoredData.excludeWords)) { throw new Error('Ungültiges Backup-Format: Keine Wörter-Liste gefunden'); } // Restore excludeWords GM_setValue('excludeWords', restoredData.excludeWords); localStorage.setItem('excludeWords', JSON.stringify(restoredData.excludeWords)); excludeWords = restoredData.excludeWords; // Restore merchantsData und extrahiere IDs if (restoredData.merchantsData) { saveExcludeMerchants(restoredData.merchantsData); // excludeMerchantIDs werden automatisch in saveExcludeMerchants gesetzt } // Restore maxPrice if (typeof restoredData.maxPrice === 'number') { saveMaxPrice(restoredData.maxPrice); const maxPriceInput = document.getElementById('maxPriceFilterInput'); if (maxPriceInput) { maxPriceInput.value = restoredData.maxPrice.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); } } // Restore hideColdDeals if (typeof restoredData.hideColdDeals === 'boolean') { hideColdDeals = restoredData.hideColdDeals; GM_setValue('hideColdDeals', hideColdDeals); localStorage.setItem('hideColdDeals', hideColdDeals); const coldDealsToggle = document.getElementById('hideColdDealsToggle'); if (coldDealsToggle) { coldDealsToggle.checked = hideColdDeals; } } if (isSettingsOpen) { updateUITheme(); } processArticles(); console.log('=== Restore Complete ==='); alert('Backup wurde erfolgreich wiederhergestellt.'); } catch (error) { console.error('Restore failed:', error); alert('Fehler beim Wiederherstellen des Backups: ' + error.message); } }; reader.readAsText(file); } // --- 4. Benutzeroberfläche (UI) --- function getSubUIPosition() { if (IS_TOUCH_DEVICE) { return ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); `; } return ` position: fixed; top: 50%; left: calc(50% + 310px); transform: translate(-50%, -50%); `; } function createMerchantListUI() { const colors = getThemeColors(); merchantListDiv.style.cssText = ` ${getSubUIPosition()} padding: 15px; background: ${colors.background}; border: 1px solid ${colors.border}; border-radius: 5px; z-index: 1001; width: 300px; color: ${colors.text}; `; const currentMerchants = loadExcludeMerchants(); const merchantListHTML = currentMerchants.map(merchant => ` <div class="merchant-item" style=" display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: ${colors.itemBg}; color: ${colors.text}; border: 1px solid ${colors.border}; border-radius: 3px;"> <span>${merchant.name}</span> <button class="delete-merchant" data-id="${merchant.id}" style=" background: none; border: none; cursor: pointer; color: ${colors.text};"> <i class="fas fa-times"></i> </button> </div> `).join(''); merchantListDiv.innerHTML = ` <h4 style="margin-bottom: 10px;">Ausgeblendete Händler (${currentMerchants.length})</h4> <input type="text" id="merchantSearch" placeholder="Händler suchen..." style=" width: 100%; padding: 5px; margin-bottom: 10px; background: ${colors.inputBg}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 3px;"> <div style="margin-bottom: 15px;"> <div id="merchantList" style="margin-bottom: 10px; max-height: 200px; overflow-y: auto; padding-right: 5px;"> ${merchantListHTML} </div> <button id="clearMerchantListButton" style=" width: 100%; padding: 5px 10px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; color: ${colors.text}; border-radius: 3px; cursor: pointer; margin-top: 10px;"> <i class="fas fa-trash"></i> Alle Händler entfernen </button> </div> <div style="text-align: right;"> <button id="closeMerchantListButton" style="padding: 8px 12px; background: none; border: none; cursor: pointer;" title="Schließen"> <i class="fas fa-times"></i> </button> </div> `; // Add the div to the document body document.body.appendChild(merchantListDiv); setupClickOutsideHandler(); // Add search functionality const searchInput = document.getElementById('merchantSearch'); searchInput.addEventListener('input', (e) => { const searchTerm = e.target.value.toLowerCase(); document.querySelectorAll('.merchant-item').forEach(item => { const merchantName = item.querySelector('span').textContent.toLowerCase(); item.style.display = merchantName.includes(searchTerm) ? 'flex' : 'none'; }); }); // Add clear all button handler document.getElementById('clearMerchantListButton').addEventListener('click', () => { if (confirm('Möchten Sie wirklich alle Händler aus der Liste entfernen?')) { saveExcludeMerchants([]); document.getElementById('merchantList').innerHTML = ''; processArticles(); } }); // Update delete button handlers document.querySelectorAll('.delete-merchant').forEach(button => { button.addEventListener('click', function(e) { // Prevent event bubbling e.preventDefault(); e.stopPropagation(); // Get merchant ID to delete const deleteButton = e.target.closest('.delete-merchant'); if (!deleteButton) return; const idToDelete = deleteButton.dataset.id; // Update merchant data const merchantsData = loadExcludeMerchants(); const updatedMerchants = merchantsData.filter(m => m.id !== idToDelete); // Save updated data saveExcludeMerchants(updatedMerchants); // Remove from UI deleteButton.closest('.merchant-item').remove(); // Refresh deals processArticles(); }); }); // Update close button handlers in createMerchantListUI document.getElementById('closeMerchantListButton').addEventListener('click', (e) => { e.stopPropagation(); // Prevent event bubbling closeActiveSubUI(); }); } function createExcludeWordsUI() { const colors = getThemeColors(); wordsListDiv.style.cssText = ` ${getSubUIPosition()} padding: 15px; background: ${colors.background}; border: 1px solid ${colors.border}; border-radius: 5px; z-index: 1001; width: 300px; color: ${colors.text}; `; const currentWords = loadExcludeWords(); const wordsListHTML = currentWords.map(word => ` <div class="word-item" style=" display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: ${colors.itemBg}; color: ${colors.text}; border: 1px solid ${colors.border}; border-radius: 3px;"> <span style="word-break: break-word;">${word}</span> <button class="delete-word" data-word="${word}" style=" background: none; border: none; cursor: pointer; color: ${colors.text};"> <i class="fas fa-times"></i> </button> </div> `).join(''); wordsListDiv.innerHTML = ` <h4 style="margin-bottom: 10px;">Ausgeblendete Wörter (${currentWords.length})</h4> <input type="text" id="wordSearch" placeholder="Wörter suchen..." style=" width: 100%; padding: 5px; margin-bottom: 10px; background: ${colors.inputBg}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 3px;"> <div style="margin-bottom: 15px;"> <div id="wordsList" style="margin-bottom: 10px; max-height: 200px; overflow-y: auto; padding-right: 5px;"> ${wordsListHTML} </div> <button id="clearWordsListButton" style=" width: 100%; padding: 5px 10px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; color: ${colors.text}; border-radius: 3px; cursor: pointer; margin-top: 10px;"> <i class="fas fa-trash"></i> Alle Wörter entfernen </button> </div> <div style="text-align: right;"> <button id="closeWordsListButton" style="padding: 8px 12px; background: none; border: none; cursor: pointer;" title="Schließen"> <i class="fas fa-times"></i> </button> </div> `; // Add the div to the document body document.body.appendChild(wordsListDiv); setupClickOutsideHandler(); // Add search functionality const searchInput = document.getElementById('wordSearch'); searchInput.addEventListener('input', (e) => { const searchTerm = e.target.value.toLowerCase(); document.querySelectorAll('.word-item').forEach(item => { const word = item.querySelector('span').textContent.toLowerCase(); item.style.display = word.includes(searchTerm) ? 'flex' : 'none'; }); }); // Add clear all button handler document.getElementById('clearWordsListButton').addEventListener('click', () => { if (confirm('Möchten Sie wirklich alle Wörter aus der Liste entfernen?')) { saveExcludeWords([]); document.getElementById('wordsList').innerHTML = ''; excludeWords = []; processArticles(); } }); // Add delete handlers document.querySelectorAll('.delete-word').forEach(button => { button.addEventListener('click', (e) => { // Prevent event bubbling e.preventDefault(); e.stopPropagation(); const deleteButton = e.target.closest('.delete-word'); if (!deleteButton) return; const wordToDelete = deleteButton.dataset.word; excludeWords = excludeWords.filter(word => word !== wordToDelete); saveExcludeWords(excludeWords); // Remove only the specific word item deleteButton.closest('.word-item').remove(); // Update deals without closing UI processArticles(); }); }); // Update close button handlers in createExcludeWordsUI document.getElementById('closeWordsListButton').addEventListener('click', (e) => { e.stopPropagation(); // Prevent event bubbling closeActiveSubUI(); }); } function getWordsFromTitle(dealElement) { // Early return checks if (!dealElement) return []; const titleElement = dealElement.querySelector('.thread-title'); if (!titleElement) return []; // 1. Basic text cleanup const titleText = decodeHtml(titleElement.textContent) .replace(/ /g, ' ') .replace(/\s+/g, ' ') .trim(); // 2. Split title into potential words // Erweitere die Split-Pattern um das Pipe-Symbol const rawWords = titleText.split(/[\s,/»«\[\](){}|]/); // 3. Process each word const words = rawWords .map(word => { // Remove leading/trailing spaces and pipes word = word.trim().replace(/\|/g, ''); // Handle hyphens: // If word starts with hyphen and has no other hyphens, // remove the leading hyphen (e.g. "-Tortilla" -> "Tortilla") if (word.startsWith('-') && !word.slice(1).includes('-')) { word = word.slice(1); } return word; }) .filter(word => word.length >= 2 && // Min length check !word.includes('=') && !word.includes('»') && !word.includes('«') && !word.includes('|') && // Extra check für Pipe-Symbol !word.startsWith('class') && !word.startsWith('title') ); return [...new Set(words)]; // Entferne Duplikate } function createSettingsUI() { if (isSettingsOpen) return; isSettingsOpen = true; // Initialize containers initUIContainers(); const colors = getThemeColors(); // Get merchant info from current deal let merchantName = null; let showMerchantButton = false; settingsDiv.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 15px; background: ${colors.background}; border: 1px solid ${colors.border}; border-radius: 5px; z-index: 1000; width: 300px; max-height: 90vh; overflow: visible; color: ${colors.text}; `; if (dealThatOpenedSettings) { const merchantLink = dealThatOpenedSettings.querySelector('a[data-t="merchantLink"]'); if (merchantLink) { merchantName = merchantLink.textContent.trim(); showMerchantButton = true; } } // Process articles when opening settings processArticles(); const currentExcludeWords = JSON.parse(localStorage.getItem(EXCLUDE_WORDS_KEY)) || []; const currentExcludeMerchantIDs = JSON.parse(localStorage.getItem(EXCLUDE_MERCHANTS_KEY)) || []; const dealWords = dealThatOpenedSettings ? getWordsFromTitle(dealThatOpenedSettings) : []; // Conditional merchant button HTML - only show if merchant exists const merchantButtonHtml = showMerchantButton ? ` <button id="hideMerchantButton" style=" width: 100%; margin-top: 5px; padding: 5px 10px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; border-radius: 3px; cursor: pointer; color: ${colors.text}; "> <i class="fas fa-store-slash"></i> Alle Deals von <span style="font-weight: bold">${merchantName}</span> ausblenden </button> ` : ''; const wordInputSection = ` <div style="margin-bottom: 20px;"> <div style="display: flex; align-items: center; gap: 4px;"> <input id="newWordInput" autocomplete="off" ${IS_TOUCH_DEVICE ? 'readonly' : ''} placeholder="Neues Wort..." title="Deals mit hier eingetragenen Wörtern im Titel werden ausgeblendet." style=" flex: 1; min-width: 0; /* Verhindert Überbreite */ padding: 8px; background: ${colors.inputBg}; border: 1px solid ${colors.border}; border-radius: 3px; color: ${colors.text}; "> ${IS_TOUCH_DEVICE ? ` <button id="enableKeyboardButton" style=" flex-shrink: 0; width: 36px; padding: 8px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; border-radius: 3px; cursor: pointer; color: ${colors.text}; "> <i class="fas fa-keyboard"></i> </button> ` : ''} <button id="addWordButton" style=" flex-shrink: 0; width: 36px; padding: 8px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; border-radius: 3px; cursor: pointer; color: ${colors.text}; "> <i class="fas fa-plus"></i> </button> </div> </div>`; settingsDiv.innerHTML = ` <h4 style="margin-bottom: 15px; color: ${colors.text}">Einstellungen zum Ausblenden</h4> ${wordInputSection} ${merchantButtonHtml} <!-- List Management Section --> <div style="margin-top: 20px; display: flex; flex-direction: column; gap: 10px;"> <button id="showWordsListButton" style=" width: 100%; padding: 8px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; border-radius: 3px; cursor: pointer; color: ${colors.text};"> <i class="fas fa-list"></i> Wortfilter verwalten </button> <button id="showMerchantListButton" style=" width: 100%; padding: 8px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; border-radius: 3px; cursor: pointer; color: ${colors.text};"> <i class="fas fa-store"></i> Händlerfilter verwalten </button> </div> <!-- Action Buttons --> <div style="margin-top: 20px; text-align: right; display: flex; justify-content: flex-end; gap: 5px;"> <button id="createBackupButton" style="padding: 8px; background: none; border: none; cursor: pointer; color: ${colors.text};" title="Backup erstellen"> <i class="fas fa-file-export"></i> </button> <button id="restoreBackupButton" style="padding: 8px; background: none; border: none; cursor: pointer; color: ${colors.text};" title="Wiederherstellen"> <i class="fas fa-file-import"></i> </button> <input type="file" id="restoreFileInput" style="display: none;" /> <button id="closeSettingsButton" style="padding: 8px; background: none; border: none; cursor: pointer; color: ${colors.text};" title="Schließen"> <i class="fas fa-times"></i> </button> </div>`; // Explicitly add to DOM document.body.appendChild(settingsDiv); if (IS_TOUCH_DEVICE) { const input = document.getElementById('newWordInput'); const keyboardButton = document.getElementById('enableKeyboardButton'); if (input && keyboardButton) { let keyboardEnabled = false; let ignoreNextFocus = false; // Focus handler für Input input.addEventListener('focus', (e) => { if (ignoreNextFocus) { ignoreNextFocus = false; return; } if (!keyboardEnabled) { // Verhindern dass die Tastatur erscheint wenn nicht explizit aktiviert e.preventDefault(); input.blur(); // Zeige Wortvorschläge if (suggestedWords.length === 0) { suggestedWords = getWordsFromTitle(dealThatOpenedSettings); } if (suggestedWords.length > 0) { updateSuggestionList(); } } }); // Keyboard Button Handler keyboardButton.addEventListener('click', () => { const input = document.getElementById('newWordInput'); if (!input) return; // Entferne readonly und aktiviere Tastatur input.removeAttribute('readonly'); keyboardEnabled = true; // Verstecke Wortvorschläge const suggestionList = document.getElementById('wordSuggestionList'); if (suggestionList) { suggestionList.remove(); } // Verhindern dass der nächste Focus die Wortvorschläge öffnet ignoreNextFocus = true; // Fokussiere Input und öffne Tastatur input.focus(); // Setze einen Timer um keyboardEnabled zurückzusetzen setTimeout(() => { keyboardEnabled = false; }, 100); }); } } setupClickOutsideHandler(); updateUITheme(); const actionButtons = settingsDiv.querySelectorAll('#closeSettingsButton, #createBackupButton, #restoreBackupButton'); actionButtons.forEach(btn => { btn.style.cssText = ` padding: 8px; background: none; border: none; cursor: pointer; color: ${colors.text}; `; }); // Add word input handler const addWordButton = document.getElementById('addWordButton'); if (addWordButton) { addWordButton.addEventListener('click', () => { const newWordInput = document.getElementById('newWordInput'); const newWord = newWordInput.value.trim(); // Lade aktuelle Wörter neu um sicherzustellen dass wir die komplette Liste haben excludeWords = loadExcludeWords(); // Prüfe ob das Wort (unabhängig von Groß-/Kleinschreibung) bereits existiert const wordExists = excludeWords.some(word => word.toLowerCase() === newWord.toLowerCase()); if (newWord && !wordExists) { excludeWords.unshift(newWord); // Füge neues Wort zur bestehenden Liste hinzu saveExcludeWords(excludeWords); newWordInput.value = ''; processArticles(); cleanup(); suggestedWords = []; const suggestionList = document.getElementById('wordSuggestionList'); if (suggestionList) { suggestionList.remove(); } } else if (wordExists) { // Erstelle und zeige Fehlermeldung const errorMsg = document.createElement('div'); errorMsg.style.cssText = ` position: absolute; top: 100%; left: 0; right: 0; padding: 8px; margin-top: 4px; background: #ffebee; color: #c62828; border: 1px solid #ef9a9a; border-radius: 3px; font-size: 12px; z-index: 1003; `; errorMsg.textContent = `"${newWord}" ist bereits in der Liste vorhanden.`; // Füge Fehlermeldung zum Input-Container hinzu const inputContainer = newWordInput.parentElement; inputContainer.style.position = 'relative'; inputContainer.appendChild(errorMsg); // Entferne Fehlermeldung nach 3 Sekunden setTimeout(() => { errorMsg.remove(); }, 3000); // Selektiere den Text im Input für einfaches Überschreiben newWordInput.select(); } }); } // Add enter key handler for input document.getElementById('newWordInput').addEventListener('keypress', (e) => { if (e.key === 'Enter') { document.getElementById('addWordButton').click(); } }); // Only add merchant button listener if button exists const hideMerchantButton = document.getElementById('hideMerchantButton'); if (hideMerchantButton && showMerchantButton) { hideMerchantButton.addEventListener('click', () => { if (!dealThatOpenedSettings) return; const merchantLink = dealThatOpenedSettings.querySelector('a[href*="merchant-id="]'); if (!merchantLink) return; const merchantIDMatch = merchantLink.getAttribute('href').match(/merchant-id=(\d+)/); if (!merchantIDMatch) return; const merchantID = merchantIDMatch[1]; const merchantName = dealThatOpenedSettings.querySelector('a[data-t="merchantLink"]').textContent.trim(); const merchantsData = loadExcludeMerchants(); if (!merchantsData.some(m => m.id === merchantID)) { merchantsData.unshift({ id: merchantID, name: merchantName }); saveExcludeMerchants(merchantsData); processArticles(); cleanup(); // Close settings UI // Aktualisiere Listen wenn UI offen if (activeSubUI === 'merchant') { updateActiveLists(); } } }); } // Add merchant list button listener document.getElementById('showMerchantListButton').addEventListener('click', () => { const btn = document.getElementById('showMerchantListButton'); if (btn.hasAttribute('data-processing')) return; btn.setAttribute('data-processing', 'true'); if (activeSubUI === 'merchant') { closeActiveSubUI(); btn.innerHTML = '<i class="fas fa-store"></i> Händlerfilter verwalten'; activeSubUI = null; } else { closeActiveSubUI(); createMerchantListUI(); activeSubUI = 'merchant'; btn.innerHTML = '<i class="fas fa-times"></i> Händlerfilter ausblenden'; } btn.removeAttribute('data-processing'); }); // Add words list button listener document.getElementById('showWordsListButton').addEventListener('click', () => { const btn = document.getElementById('showWordsListButton'); if (activeSubUI === 'words') { closeActiveSubUI(); btn.innerHTML = '<i class="fas fa-list"></i> Wortfilter verwalten'; activeSubUI = null; } else { closeActiveSubUI(); createExcludeWordsUI(); activeSubUI = 'words'; btn.innerHTML = '<i class="fas fa-times"></i> Wortfilter ausblenden'; } }); // Always ensure close button works document.getElementById('closeSettingsButton').addEventListener('click', (e) => { e.stopPropagation(); // Prevent event bubbling cleanup(); }); // Backup/Restore Event Listeners document.getElementById('createBackupButton').addEventListener('click', backupData); document.getElementById('restoreBackupButton').addEventListener('click', () => { document.getElementById('restoreFileInput').click(); }); document.getElementById('restoreFileInput').addEventListener('change', restoreData); // Add event listeners only if newWordInput exists const newWordInput = document.getElementById('newWordInput'); if (newWordInput) { // Unified focus handler newWordInput.addEventListener('focus', () => { // Get fresh words from current deal if none exist if (suggestedWords.length === 0) { suggestedWords = getWordsFromTitle(dealThatOpenedSettings); } // Always show suggestion list if words exist if (suggestedWords.length > 0) { updateSuggestionList(); } }, { once: false }); // Allow multiple focus events } // Click Outside Handler anpassen createSuggestionClickHandler(); // Cleanup bei UI-Schließung document.getElementById('closeSettingsButton').addEventListener('click', () => { document.removeEventListener('click', suggestionClickHandler); }); // Add cleanup to window unload window.addEventListener('unload', cleanup); const maxPriceInput = document.getElementById('maxPriceFilterInput'); // Note the correct ID if (maxPriceInput) { maxPriceInput.addEventListener('change', (e) => { const price = parseFloat(e.target.value); if (!isNaN(price) && price >= 0) { saveMaxPrice(price); processArticles(); } }); } // Get initial word suggestions suggestedWords = dealThatOpenedSettings ? getWordsFromTitle(dealThatOpenedSettings) : []; } function updateSuggestionList() { // Save scroll position if list exists const oldList = document.getElementById('wordSuggestionList'); const scrollPosition = oldList?.scrollTop || 0; // Remove old list if exists if (oldList) oldList.remove(); // Filter and check for words suggestedWords = suggestedWords.filter(word => !excludeWords.includes(word)); if (!suggestedWords.length) return; const inputField = document.getElementById('newWordInput'); const inputRect = inputField.getBoundingClientRect(); const colors = getThemeColors(); // Create suggestion list with fixed positioning const wordSuggestionList = document.createElement('div'); wordSuggestionList.id = 'wordSuggestionList'; wordSuggestionList.style.cssText = ` position: fixed; top: ${inputRect.bottom}px; left: ${inputRect.left}px; width: ${inputRect.width}px; max-height: 200px; overflow-y: auto; background: ${colors.background}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 3px; z-index: 1002; box-shadow: 0 2px 5px rgba(0,0,0,0.1); display: block; `; // Add scroll event listener to update position const updatePosition = () => { const newRect = inputField.getBoundingClientRect(); wordSuggestionList.style.top = `${newRect.bottom}px`; wordSuggestionList.style.left = `${newRect.left}px`; }; window.addEventListener('scroll', updatePosition, true); window.addEventListener('resize', updatePosition); // Rest of the function stays the same wordSuggestionList.innerHTML = suggestedWords .map(word => ` <div class="word-suggestion-item" style="padding: 10px; border-bottom: 1px solid #eee; cursor: pointer; transition: background-color 0.2s;"> ${word} </div> `).join(''); document.body.appendChild(wordSuggestionList); wordSuggestionList.scrollTop = scrollPosition; // Add event listeners for items wordSuggestionList.querySelectorAll('.word-suggestion-item').forEach(item => { item.addEventListener('mouseenter', () => { item.style.backgroundColor = colors.itemBg; }); item.addEventListener('mouseleave', () => { item.style.backgroundColor = colors.background; }); item.addEventListener('click', handleWordSelection); }); // Clean up event listeners when list is removed const cleanupListeners = () => { window.removeEventListener('scroll', updatePosition, true); window.removeEventListener('resize', updatePosition); }; // Add to existing cleanup function const oldCleanup = cleanup; cleanup = () => { cleanupListeners(); oldCleanup(); }; } function initObserver() { // Disconnect existing observer if it exists if (observer) { observer.disconnect(); } // Reinitialize the observer observer.observe(document.body, { childList: true, subtree: true }); // Process any existing articles processArticles(); addSettingsButton(); addHideButtons(); } // Update addMerchantToList function function addMerchantToList(merchant, merchantList) { const div = document.createElement('div'); div.className = 'merchant-item'; div.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: #f0f0f0; border-radius: 3px;'; div.innerHTML = ` <span style="font-weight: bold;">${merchant.name}</span> <button class="delete-merchant" data-id="${merchant.id}" style="background: none; border: none; cursor: pointer; color: #666;"> <i class="fas fa-times"></i> </button> `; // Insert at beginning of list merchantList.insertBefore(div, merchantList.firstChild); } // Update word list UI - add new item at top function addWordToList(word, wordsList) { const div = document.createElement('div'); div.className = 'word-item'; div.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: #f0f0f0; border-radius: 3px;'; div.innerHTML = ` <span style="word-break: break-word;">${word}</span> <button class="delete-word" data-word="${word}" style="background: none; border: none; cursor: pointer; color: #666;"> <i class="fas fa-times"></i> </button> `; // Insert at beginning of list wordsList.insertBefore(div, wordsList.firstChild); } function setupClickOutsideHandler() { if (uiClickOutsideHandler) { document.removeEventListener('click', uiClickOutsideHandler); } uiClickOutsideHandler = (e) => { // Early exit for clicks on UI controls if (e.target.closest('.settings-button') || e.target.closest('#showMerchantListButton') || e.target.closest('#showWordsListButton')) { return; } // Get current UI states const settingsOpen = settingsDiv?.parentNode; const merchantsOpen = merchantListDiv?.parentNode; const wordsOpen = wordsListDiv?.parentNode; // Check if click was outside all UIs const clickedOutside = (!settingsOpen || !settingsDiv.contains(e.target)) && (!merchantsOpen || !merchantListDiv.contains(e.target)) && (!wordsOpen || !wordsListDiv.contains(e.target)); if (clickedOutside) { cleanup(); // Explicit cleanup of UI elements if (settingsDiv?.parentNode) settingsDiv.remove(); if (merchantListDiv?.parentNode) merchantListDiv.remove(); if (wordsListDiv?.parentNode) wordsListDiv.remove(); // Reset states isSettingsOpen = false; activeSubUI = null; // Remove handler document.removeEventListener('click', uiClickOutsideHandler); uiClickOutsideHandler = null; } }; // Add with delay to prevent immediate trigger setTimeout(() => { document.addEventListener('click', uiClickOutsideHandler); }, 100); } // Add helper function to close sub-UIs function closeActiveSubUI() { if (activeSubUI === 'merchant') { merchantListDiv?.remove(); const btn = document.getElementById('showMerchantListButton'); if (btn) { btn.innerHTML = '<i class="fas fa-store"></i> Händlerfilter verwalten'; btn.removeAttribute('data-processing'); } } else if (activeSubUI === 'words') { wordsListDiv?.remove(); const btn = document.getElementById('showWordsListButton'); if (btn) { btn.innerHTML = '<i class="fas fa-list"></i> Wortfilter verwalten'; } } activeSubUI = null; } // Add new function to handle merchant pages function addMerchantPageHideButton() { // Check if we're on a merchant page const urlParams = new URLSearchParams(window.location.search); const merchantId = urlParams.get('merchant-id'); const merchantBanner = document.querySelector(MERCHANT_PAGE_SELECTOR); const merchantName = document.querySelector('.merchant-banner__title')?.textContent.trim(); if (!merchantId || !merchantBanner || !merchantName) return; // Create hide button container const hideButtonContainer = document.createElement('div'); hideButtonContainer.style.cssText = ` display: inline-flex; align-items: center; margin-left: 10px; `; // Create hide button const hideButton = document.createElement('button'); hideButton.innerHTML = '<i class="fas fa-store-slash"></i>'; hideButton.title = `Alle Deals von ${merchantName} ausblenden`; hideButton.style.cssText = ` padding: 8px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; `; // Add click handler hideButton.addEventListener('click', () => { const merchantsData = loadExcludeMerchants(); // Check if ID already exists if (!merchantsData.some(m => m.id === merchantId)) { // Add new merchant at start of array merchantsData.unshift({ id: merchantId, name: merchantName }); saveExcludeMerchants(merchantsData); processArticles(); } }); // Add button to page hideButtonContainer.appendChild(hideButton); merchantBanner.appendChild(hideButtonContainer); } // Funktion zum Aktualisieren der aktiven Listen function updateActiveLists() { const colors = getThemeColors(); if (activeSubUI === 'merchant' && merchantListDiv) { const merchantList = document.getElementById('merchantList'); if (merchantList) { const currentMerchants = loadExcludeMerchants(); merchantList.innerHTML = currentMerchants.map(merchant => ` <div class="merchant-item" style=" display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: ${colors.itemBg}; color: ${colors.text}; border: 1px solid ${colors.border}; border-radius: 3px;"> <div style="display: flex; flex-direction: column;"> <span>${merchant.name}</span> <span style="color: ${colors.text}; opacity: 0.7; font-size: 0.8em;">ID: ${merchant.id}</span> </div> <button class="delete-merchant" data-id="${merchant.id}" style=" background: none; border: none; cursor: pointer; color: ${colors.text};"> <i class="fas fa-times"></i> </button> </div> `).join(''); // Event Listener neu hinzufügen document.querySelectorAll('.delete-merchant').forEach(button => { button.addEventListener('click', handleMerchantDelete); }); } } else if (activeSubUI === 'words' && wordsListDiv) { const wordsList = document.getElementById('wordsList'); if (wordsList) { const currentWords = loadExcludeWords(); wordsList.innerHTML = currentWords.map(word => ` <div class="word-item" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: #f0f0f0; border-radius: 3px;"> <span style="word-break: break-word;">${word}</span> <button class="delete-word" data-word="${word}" style="background: none; border: none; cursor: pointer; color: #666;"> <i class="fas fa-times"></i> </button> </div> `).join(''); // Event Listener neu hinzufügen document.querySelectorAll('.delete-word').forEach(button => { button.addEventListener('click', handleWordDelete); }); } } } // Handler Funktionen definieren function handleMerchantDelete(e) { e.preventDefault(); e.stopPropagation(); const deleteButton = e.target.closest('.delete-merchant'); if (!deleteButton) return; const idToDelete = deleteButton.dataset.id; const merchantsData = loadExcludeMerchants(); const updatedMerchants = merchantsData.filter(m => m.id !== idToDelete); saveExcludeMerchants(updatedMerchants); deleteButton.closest('.merchant-item').remove(); processArticles(); } function handleWordDelete(e) { e.preventDefault(); e.stopPropagation(); const deleteButton = e.target.closest('.delete-word'); if (!deleteButton) return; const wordToDelete = deleteButton.dataset.word; excludeWords = excludeWords.filter(word => word !== wordToDelete); saveExcludeWords(excludeWords); deleteButton.closest('.word-item').remove(); processArticles(); } // Add new save function function saveMaxPrice(price) { GM_setValue('maxPrice', price.toString()); localStorage.setItem('maxPrice', price.toString()); maxPrice = price; } // Add after other utility functions function handleWordSelection(e) { e.preventDefault(); e.stopPropagation(); const wordSuggestionList = document.getElementById('wordSuggestionList'); const scrollPosition = wordSuggestionList.scrollTop; // Save scroll position const word = e.target.textContent.trim(); const newWordInput = document.getElementById('newWordInput'); const currentValue = newWordInput.value.trim(); newWordInput.value = currentValue ? `${currentValue} ${word}` : word; suggestedWords = suggestedWords.filter(w => w !== word); updateSuggestionList(); newWordInput.focus(); // Restore scroll position after list update const updatedList = document.getElementById('wordSuggestionList'); if (updatedList) { updatedList.scrollTop = scrollPosition; } } // Add after other global functions function cleanup() { // Remove settings UI if (settingsDiv?.parentNode) { settingsDiv.remove(); isSettingsOpen = false; } // Add word suggestion list cleanup const suggestionList = document.getElementById('wordSuggestionList'); if (suggestionList) { suggestionList.remove(); } // Close merchant & words lists if (merchantListDiv?.parentNode) merchantListDiv.remove(); if (wordsListDiv?.parentNode) wordsListDiv.remove(); // Reset UI states if (activeSubUI === 'merchant' || activeSubUI === 'words') { const btn = document.getElementById(`show${activeSubUI === 'merchant' ? 'Merchant' : 'Words'}ListButton`); if (btn) { btn.innerHTML = activeSubUI === 'merchant' ? '<i class="fas fa-store"></i> Händlerfilter verwalten' : '<i class="fas fa-list"></i> Wortfilter verwalten'; btn.removeAttribute('data-processing'); } } activeSubUI = null; // Clean up handlers document.removeEventListener('click', suggestionClickHandler); document.removeEventListener('click', uiClickOutsideHandler); window.removeEventListener('unload', cleanup); uiClickOutsideHandler = null; // Reset suggestion state suggestedWords = []; // Don't disconnect the main observer // Instead, reinitialize it to ensure it's working initObserver(); } // 4. Complete UI state reset function resetUIState() { isSettingsOpen = false; activeSubUI = null; dealThatOpenedSettings = null; suggestedWords = []; initialSuggestionListCreated = false; settingsDiv?.remove(); merchantListDiv?.remove(); wordsListDiv?.remove(); } // Add after other global functions function createSuggestionClickHandler() { // Remove old handler if exists if (suggestionClickHandler) { document.removeEventListener('click', suggestionClickHandler); } suggestionClickHandler = (e) => { const list = document.getElementById('wordSuggestionList'); const input = document.getElementById('newWordInput'); if (!list?.contains(e.target) && !input?.contains(e.target)) { list?.remove(); } }; document.addEventListener('click', suggestionClickHandler); return suggestionClickHandler; } // Add function to inject max price filter function injectMaxPriceFilter() { const filterForm = document.querySelector('.subNavMenu-list form:first-of-type ul'); if (!filterForm) return; // Get theme colors for logging const isDark = isDarkMode(); const colors = getThemeColors(); // Create filter items with optimized HTML const filterItems = document.createElement('div'); filterItems.innerHTML = ` <!-- Cold Deals Toggle --> <li class="flex boxAlign-jc--all-sb boxAlign-ai--all-c space--h-3 space--v-3 subNavMenu-item--separator"> <span class="subNavMenu-text mute--text space--r-2 overflow--wrap-off">Kalte Deals ausblenden</span> <label class="checkbox checkbox--brand checkbox--mode-special"> <input class="input checkbox-input" type="checkbox" id="hideColdDealsToggle" ${hideColdDeals ? 'checked' : ''} > <span class="tGrid-cell tGrid-cell--shrink"> <span class="checkbox-box flex--inline boxAlign-jc--all-c boxAlign-ai--all-c"> <svg width="18px" height="14px" class="icon icon--tick checkbox-tick"> <use xlink:href="/assets/img/ico_707ed.svg#tick"></use> </svg> </span> </span> </label> </li> <!-- Price Filter --> <li class="flex boxAlign-jc--all-sb boxAlign-ai--all-c space--h-3 space--v-3"> <span class="subNavMenu-text mute--text space--r-2 overflow--wrap-off"> Maximalpreis filtern </span> <input type="text" id="maxPriceFilterInput" value="${maxPrice.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 2 })}" placeholder="€" style=" width: 80px; padding: 4px 8px; border: 1px solid var(--border-color, ${isDarkMode() ? '#6b6d6d' : '#c5c7ca'}); border-radius: 4px; margin-left: auto; text-align: right; background: ${colors.inputBg} !important; color: ${colors.text} !important; font-size: 14px; line-height: 1.5; transition: border-color .15s ease-in-out; -moz-appearance: textfield; &::-webkit-outer-spin-button, &::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } " > </li>`; filterForm.appendChild(filterItems); // After input creation, get the element and check computed styles const maxPriceInput = document.getElementById('maxPriceFilterInput'); if (maxPriceInput) { const computedStyle = window.getComputedStyle(maxPriceInput); } // Optimized input handling const priceInput = document.getElementById('maxPriceFilterInput'); if (priceInput) { // Speichere den initialen Wert für Vergleiche const initialValue = maxPrice.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); const formatPrice = (value) => { // Remove all non-numeric characters except comma and dot let cleaned = value.replace(/[^\d.,]/g, ''); // Handle thousands separators and decimal const parts = cleaned.split(','); if (parts.length > 2) { cleaned = parts.slice(0, -1).join('') + ',' + parts.slice(-1); } // Format with thousand separators if (parts.length === 2) { const intPart = parts[0].replace(/\./g, ''); const formatted = Number(intPart).toLocaleString('de-DE') + ',' + parts[1]; return formatted; } else { const intPart = cleaned.replace(/\./g, ''); return Number(intPart).toLocaleString('de-DE'); } }; // Event-Handler für Input-Formatierung priceInput.addEventListener('input', (e) => { e.stopPropagation(); const formattedValue = formatPrice(e.target.value); e.target.value = formattedValue; }, { passive: true }); // Event-Handler für Blur (wenn Input den Fokus verliert) priceInput.addEventListener('blur', (e) => { const value = e.target.value; const numStr = value.replace(/\./g, '').replace(',', '.'); const numericValue = parseFloat(numStr); if (!isNaN(numericValue) && numericValue >= 0 && value !== initialValue) { saveMaxPrice(numericValue); location.reload(); } else { // Wenn ungültig, setze auf den ursprünglichen Wert zurück e.target.value = initialValue; } }); } // Cold deals toggle const coldDealsToggle = document.getElementById('hideColdDealsToggle'); if (coldDealsToggle) { coldDealsToggle.addEventListener('change', (e) => { e.stopPropagation(); hideColdDeals = e.target.checked; GM_setValue('hideColdDeals', hideColdDeals); localStorage.setItem(HIDE_COLD_DEALS_KEY, hideColdDeals); processArticles(); }); } } // Add observer for filter UI changes const filterObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.addedNodes.length) { const filterMenu = document.querySelector('.subNavMenu-list'); if (filterMenu && !document.getElementById('maxPriceFilterInput')) { injectMaxPriceFilter(); } } }); }); // Start observing filter area filterObserver.observe(document.body, { childList: true, subtree: true });