// ==UserScript== // @name mydealz Manager // @namespace http://tampermonkey.net/ // @version 1.12.9.1 // @description Deals gezielt ausblenden mittels X Button, Filtern nach Händlern und Wörtern im Titel. Teure und kalte Deals ausblenden. // @author Flo (https://www.mydealz.de/profile/Basics0119) (https://github.com/9jS2PL5T) & Moritz Baumeister (https://www.mydealz.de/profile/BobBaumeister) (https://github.com/grapefruit89) // @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 // @downloadURL none // ==/UserScript== // Versions-Änderungen // 1.12.9 // FIX: Beim Scrollen im SettingsUI wurde teilweise die Website gescrollt. // FIX: Auf Touch-Geräten war keine Eingabe im Maximalpreis Filter möglich. // FIX: Wurde während der Suche in Wort-/Händlerliste ein Eintrag gelöscht, sprang der Zähler von xx/xx auf xx und zeigte nur noch die Gesamtzahl der Einträge. // CHANGE: Optimierung der Vorschläge in der WordSuggestionList. // CHANGE: Robustere und präzisere Wortfilterung. // 1.12.9.1 // FIX: Auf Touch Geräten wurde nicht in der WordSuggestionList gescrollt. // --- 1. Initialisierung und Grundeinstellungen --- // 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); const preventAutoCloseStyle = document.createElement('style'); preventAutoCloseStyle.textContent = ` .subNavMenu.keep-open { display: block !important; visibility: visible !important; opacity: 1 !important; pointer-events: auto !important; } `; document.head.appendChild(preventAutoCloseStyle); // Add constant for touch detection const IS_TOUCH_DEVICE = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0); // Konstanten und Variablen const HIDDEN_DEALS_KEY = 'hiddenDeals'; const MERCHANT_PAGE_SELECTOR = '.merchant-banner'; const HIDE_COLD_DEALS_KEY = 'hideColdDeals'; const MAX_PRICE_KEY = 'maxPrice'; function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => { inThrottle = false; return false; }, limit); } } } // DOM-Beobachter einrichten const observer = new MutationObserver(throttle(() => { processArticles(); addSettingsButton(); addHideButtons(); }, 250)); // Filter-UI Beobachter const filterObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.addedNodes.length) { const filterMenu = document.querySelector('.subNavMenu-list'); if (filterMenu && !document.getElementById('maxPriceFilterInput')) { injectMaxPriceFilter(); } } }); }); // Globale Zustandsvariablen 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; // --- 2. Theme-System --- // Farbkonstanten für Light/Dark Mode 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' } }; // Theme-Erkennung 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; } // Theme-Farben abrufen function getThemeColors() { const isDark = isDarkMode(); return isDark ? THEME_COLORS.dark : THEME_COLORS.light; } // Theme Observer erstellen const themeObserver = new MutationObserver(() => { requestAnimationFrame(() => { const isLight = !isDarkMode(); updateAllUIThemes(isLight); }); }); // Observer für beide Elemente einrichten const targetNodes = [document.documentElement, document.body]; targetNodes.forEach(node => { themeObserver.observe(node, { attributes: true, attributeFilter: ['class', 'data-theme'] }); }); // System Theme Observer const systemThemeObserver = window.matchMedia('(prefers-color-scheme: dark)'); systemThemeObserver.addEventListener('change', () => { requestAnimationFrame(() => { const isLight = !isDarkMode(); updateAllUIThemes(isLight); }); }); // Hide Button Theme Observer 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(); } }); }); // Start observing theme changes hideButtonThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); // Theme Update Funktionen 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; }); } } // --- 3. Datenverwaltung --- // Storage-Synchronisation async function syncStorage() { // Prüfe ob Migration bereits durchgeführt wurde const migrationComplete = GM_getValue('migrationComplete', false); // Lese Daten aus beiden Speichern 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); 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'; let migrationPerformed = false; // Migriere Wörter const effectiveWords = gmExcludeWords || lsExcludeWords || []; if (effectiveWords.length > 0) { GM_setValue('excludeWords', effectiveWords); localStorage.setItem('excludeWords', JSON.stringify(effectiveWords)); excludeWords = effectiveWords; migrationPerformed = true; } // Migriere Händler const effectiveMerchants = gmExcludeMerchants || lsExcludeMerchants || []; if (effectiveMerchants.length > 0) { GM_setValue('excludeMerchantsData', effectiveMerchants); excludeMerchantIDs = effectiveMerchants.map(m => m.id); GM_setValue('excludeMerchantIDs', excludeMerchantIDs); localStorage.setItem('excludeMerchantsData', JSON.stringify(effectiveMerchants)); migrationPerformed = true; } // Migriere versteckte Deals const effectiveHiddenDeals = gmHiddenDeals || lsHiddenDeals || []; if (effectiveHiddenDeals.length > 0) { GM_setValue('hiddenDeals', effectiveHiddenDeals); localStorage.setItem('hiddenDeals', JSON.stringify(effectiveHiddenDeals)); hiddenDeals = effectiveHiddenDeals; migrationPerformed = true; } // Migriere Einstellungen if (!migrationComplete) { if (gmHideColdDeals !== null || lsHideColdDeals !== 'null') { const effectiveHideColdDeals = gmHideColdDeals ?? (lsHideColdDeals === 'true'); GM_setValue('hideColdDeals', effectiveHideColdDeals); localStorage.setItem('hideColdDeals', effectiveHideColdDeals.toString()); hideColdDeals = effectiveHideColdDeals; migrationPerformed = true; } if (gmMaxPrice !== null || lsMaxPrice !== 'null') { const effectiveMaxPrice = gmMaxPrice || lsMaxPrice; GM_setValue('maxPrice', effectiveMaxPrice); localStorage.setItem('maxPrice', effectiveMaxPrice); maxPrice = parseFloat(effectiveMaxPrice); migrationPerformed = true; } } // Markiere Migration als abgeschlossen nur wenn tatsächlich Daten migriert wurden if (migrationPerformed) { GM_setValue('migrationComplete', true); } } // Speicherfunktionen function saveHiddenDeals() { GM_setValue('hiddenDeals', hiddenDeals); localStorage.setItem('hiddenDeals', JSON.stringify(hiddenDeals)); } function saveExcludeWords(words) { // Normalisiere Groß-/Kleinschreibung und entferne Duplikate const normalizedWords = words.reduce((acc, word) => { const lowerWord = word.toLowerCase(); const exists = acc.some(w => w.toLowerCase() === lowerWord); if (!exists) { acc.push(word); // Behält originale Schreibweise bei } return acc; }, []); // Speichere nur die normalisierte Version GM_setValue('excludeWords', normalizedWords); localStorage.setItem('excludeWords', JSON.stringify(normalizedWords)); } function loadExcludeWords() { // Load from GM storage const gmWords = GM_getValue('excludeWords', []); // Load from localStorage let lsWords = []; try { lsWords = JSON.parse(localStorage.getItem('excludeWords') || '[]'); } catch (e) { } // Show final result const result = gmWords.length > 0 ? gmWords : lsWords; return result; } 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; } function saveMaxPrice(price) { GM_setValue('maxPrice', price.toString()); localStorage.setItem('maxPrice', price.toString()); maxPrice = price; } // --- 4. Kernfunktionen --- // Artikel verarbeiten und filtern 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'; }); } // Ausschlussprüfung für Artikel 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 priceText = priceElement.textContent.trim(); const priceMatch = priceText.match(/([\d.,]+)\s*€/); if (priceMatch) { const price = parseFloat(priceMatch[1].replace('.', '').replace(',', '.')); if (!isNaN(price) && price > maxPrice) return true; } } catch (error) { continue; } } } // 3. Complex checks // Get title text const rawTitle = titleElement.querySelector('a')?.getAttribute('title') || titleElement.innerText; const processedTitle = rawTitle.toLowerCase(); // Check excludeWords if (excludeWords.some(word => { const searchTerm = word.toLowerCase(); // Handle words in brackets like [CB] if (searchTerm.startsWith('[') && searchTerm.endsWith(']')) { return processedTitle.includes(searchTerm); } // Handle words with special characters (+) if (searchTerm.includes('+')) { return processedTitle.includes(searchTerm); } // Handle multi-word phrases (like "eau de toilette" or "internet radio") if (searchTerm.includes(' ') || searchTerm.includes('-')) { const variations = [ searchTerm, // original form searchTerm.replace(/-/g, ' '), // hyphen to space searchTerm.replace(/-/g, ''), // without hyphen searchTerm.replace(/ /g, ''), // without spaces searchTerm.replace(/-/g, ' ').trim(), // normalized spaces searchTerm.replace(/ /g, '-') // space to hyphen ]; // Remove duplicates const uniqueVariations = [...new Set(variations)]; return uniqueVariations.some(variant => { // For multi-word variations, all words must be present in order if (variant.includes(' ')) { const words = variant.split(' ').filter(w => w.length > 0); let lastIndex = -1; return words.every(word => { const index = processedTitle.indexOf(word, lastIndex + 1); if (index === -1) return false; lastIndex = index; return true; }); } // For single words or compound words return processedTitle.includes(variant); }); } // Use word boundaries for normal words const regex = new RegExp(`\\b${searchTerm}\\b`, 'i'); return regex.test(processedTitle); })) 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'; } function getWordsFromTitle(deal) { const titleElement = deal.querySelector('.thread-title'); if (!titleElement) return []; const rawTitle = titleElement.querySelector('a')?.getAttribute('title') || titleElement.innerText || ''; const keepWords = ['von', 'der', 'die', 'das', 'bei', 'mit', 'und', 'oder', 'auf', 'für', 'durch', 'bis', 'ab']; const ignoreWords = ['Euro', 'EUR', 'VSK', '€', 'VGP', 'cent', 'Cent']; const ignoreChars = ['&', '+', '!', '-', '/', '%', '–']; const units = ['MB/s', 'GB/s', 'KB/s', 'Mbit/s', 'Gbit/s', 'Kbit/s']; const priceContextWords = ['effektiv']; const isDate = (word) => { return /^\d{1,2}[.,]\d{1,2}(?:[.,]\d{2,4})?$/.test(word); }; const isPriceContext = (word) => { if (!priceContextWords.includes(word.toLowerCase())) return false; // Prüfe ob im Titel ein Preis vorkommt const hasPricePattern = /\d+(?:[.,]\d{2})?(?:€|EUR|Euro)/i; return hasPricePattern.test(rawTitle); }; const isPrice = (word) => { return /^~?\d+(?:[.,]\d{2})?(?:€|EUR)?$/.test(word) || /^\d+(?:[.,]\d{2})?(?:\s*cent|\s*Cent)$/i.test(word); }; const isPercentage = (word) => { return /^\d+\s*%?$/.test(word) && rawTitle.includes('%'); }; const cleanWord = (word) => { // Prüfe erst auf bekannte Einheiten if (units.some(unit => word.includes(unit))) { // Entferne nur Sonderzeichen am Ende die nicht zur Einheit gehören const cleanedWord = word.trim(); return cleanedWord.replace(/[,;:!?.]+$/, ''); } return word .trim() .replace(/^[^a-zA-Z0-9äöüÄÖÜß]+|[^a-zA-Z0-9äöüÄÖÜß]+$/g, '') .replace(/^[&+!%–]+$/, '') .replace(/[-,]+$/, ''); }; const shouldKeepWord = (word) => { const lowerWord = word.toLowerCase(); if (!word || word.length === 0) return false; if (ignoreChars.includes(word)) return false; if (ignoreWords.some(ignore => ignore.toLowerCase() === lowerWord)) return false; if (isDate(word)) return false; if (isPrice(word)) return false; if (isPercentage(word)) return false; if (isPriceContext(word)) return false; // Behalte spezielle Wörter if (keepWords.includes(lowerWord)) return true; if (units.some(unit => word === unit)) return true; return true; }; const splitTitle = (title) => { // Temporär Einheiten ersetzen let tempTitle = title; const replacements = new Map(); units.forEach((unit, index) => { const placeholder = `__UNIT${index}__`; while (tempTitle.includes(unit)) { tempTitle = tempTitle.replace(unit, placeholder); replacements.set(placeholder, unit); } }); // Split und Einheiten wiederherstellen return tempTitle .split(/[\s\/]+/) .map(word => { replacements.forEach((unit, placeholder) => { if (word.includes(placeholder)) { word = word.replace(placeholder, unit); } }); return word; }); }; return splitTitle(rawTitle) .map(cleanWord) .filter(shouldKeepWord) .filter((word, index, self) => self.indexOf(word) === index); } // --- 5. Benutzeroberfläche (UI) --- function setupScrollHandling() { let isScrollingUI = false; let lastActiveUI = null; let touchStartY = 0; function handleMouseEnter(e) { const targetUI = e.currentTarget; if (targetUI) { lastActiveUI = targetUI; } } function handleMouseLeave() { lastActiveUI = null; } function handleScroll(e) { // Check if scrolling happens in a scrollable list const scrollableElement = e.target.closest('#merchantList, #wordsList, #wordSuggestionList'); if (scrollableElement) { // Allow scrolling within scrollable lists const isAtTop = scrollableElement.scrollTop === 0; const isAtBottom = scrollableElement.scrollTop + scrollableElement.clientHeight >= scrollableElement.scrollHeight; // Only prevent scrolling at top/bottom of list if ((isAtTop && e.deltaY < 0) || (isAtBottom && e.deltaY > 0)) { e.preventDefault(); e.stopPropagation(); } return; // Exit early for scrollable elements } if (IS_TOUCH_DEVICE) { if (isScrollingUI) { e.preventDefault(); e.stopPropagation(); } return; } // Desktop handling if (lastActiveUI) { const rect = lastActiveUI.getBoundingClientRect(); const mouseIsOverUI = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom; if (mouseIsOverUI && !e.target.closest('#merchantList, #wordsList, #wordSuggestionList')) { e.preventDefault(); e.stopPropagation(); } } } function handleTouchStart(e) { const touch = e.touches[0]; touchStartY = touch.clientY; const uiElements = [settingsDiv, merchantListDiv, wordsListDiv, document.getElementById('wordSuggestionList')]; isScrollingUI = uiElements.some(el => { if (!el?.parentNode) return false; const rect = el.getBoundingClientRect(); return touch.clientX >= rect.left && touch.clientX <= rect.right && touch.clientY >= rect.top && touch.clientY <= rect.bottom; }); } function handleTouchMove(e) { if (!isScrollingUI) return; const touch = e.touches[0]; const scrollableElement = e.target.closest('#merchantList, #wordsList'); if (scrollableElement) { const deltaY = touchStartY - touch.clientY; const isAtTop = scrollableElement.scrollTop === 0; const isAtBottom = scrollableElement.scrollTop + scrollableElement.clientHeight >= scrollableElement.scrollHeight; // Erlaube Scrollen in der Liste wenn nicht am Anfang/Ende if ((isAtTop && deltaY < 0) || (isAtBottom && deltaY > 0)) { e.preventDefault(); } } else { // Blockiere Scrollen außerhalb der Listen e.preventDefault(); } touchStartY = touch.clientY; } function setupUIElement(element) { if (!element?.parentNode) return; element.addEventListener('mouseenter', handleMouseEnter); element.addEventListener('mouseleave', handleMouseLeave); } function setupAllElements() { // Füge wordSuggestionList zu den zu überwachenden Elementen hinzu [settingsDiv, merchantListDiv, wordsListDiv, document.getElementById('wordSuggestionList')] .forEach(setupUIElement); } // Initial Setup setupAllElements(); // Event Listener if (IS_TOUCH_DEVICE) { document.addEventListener('touchstart', handleTouchStart, { passive: true }); document.addEventListener('touchmove', handleTouchMove, { passive: false }); } document.addEventListener('wheel', handleScroll, { passive: false }); // MutationObserver für dynamisch hinzugefügte UIs const observer = new MutationObserver(setupAllElements); observer.observe(document.body, { childList: true, subtree: true }); // Cleanup-Funktion return () => { if (IS_TOUCH_DEVICE) { document.removeEventListener('touchstart', handleTouchStart); document.removeEventListener('touchmove', handleTouchMove); } document.removeEventListener('wheel', handleScroll); [settingsDiv, merchantListDiv, wordsListDiv, document.getElementById('wordSuggestionList')] .forEach(el => { if (el?.parentNode) { el.removeEventListener('mouseenter', handleMouseEnter); el.removeEventListener('mouseleave', handleMouseLeave); } }); observer.disconnect(); }; } // UI-Basis function initUIContainers() { settingsDiv = document.createElement('div'); merchantListDiv = document.createElement('div'); wordsListDiv = document.createElement('div'); } // Einstellungsfenster erstellen 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(); // Conditional merchant button HTML - only show if merchant exists const merchantButtonHtml = showMerchantButton ? ` ` : ''; const wordInputSection = `
${IS_TOUCH_DEVICE ? ` ` : ''}
`; settingsDiv.innerHTML = `

Einstellungen zum Ausblenden

${wordInputSection} ${merchantButtonHtml}
`; // 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 = ' Händlerfilter verwalten'; activeSubUI = null; } else { closeActiveSubUI(); createMerchantListUI(); activeSubUI = 'merchant'; btn.innerHTML = ' 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 = ' Wortfilter verwalten'; activeSubUI = null; } else { closeActiveSubUI(); createExcludeWordsUI(); activeSubUI = 'words'; btn.innerHTML = ' 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) : []; // Scroll-Handling einrichten const cleanupScrollHandling = setupScrollHandling(); // Cleanup erweitern const oldCleanup = cleanup; cleanup = () => { cleanupScrollHandling(); oldCleanup(); }; } // Listen-Management // Händlerliste erstellen 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 => `
${merchant.name}
`).join(''); merchantListDiv.innerHTML = `

Ausgeblendete Händler (${currentMerchants.length})

${merchantListHTML}
`; // 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(); let visibleCount = 0; // Hole aktuelle Händler statt die ursprüngliche Liste zu verwenden const currentMerchants = loadExcludeMerchants(); const totalCount = currentMerchants.length; document.querySelectorAll('.merchant-item').forEach(item => { const merchantName = item.querySelector('span').textContent.toLowerCase(); const isVisible = merchantName.includes(searchTerm); item.style.display = isVisible ? 'flex' : 'none'; if (isVisible) visibleCount++; }); // Update heading counter const heading = merchantListDiv.querySelector('h4'); if (heading) { heading.textContent = searchTerm ? `Ausgeblendete Händler (${visibleCount}/${totalCount})` : `Ausgeblendete Händler (${totalCount})`; } }); // Alle Händler entfernen Button document.getElementById('clearMerchantListButton').addEventListener('click', () => { if (confirm('Möchten Sie wirklich alle Händler aus der Liste entfernen?')) { saveExcludeMerchants([]); document.getElementById('merchantList').innerHTML = ''; excludeMerchantIDs = []; processArticles(); // Immediately update counter in heading const heading = merchantListDiv.querySelector('h4'); if (heading) { heading.textContent = 'Ausgeblendete Händler (0)'; } } }); document.querySelectorAll('.delete-merchant').forEach(button => { button.addEventListener('click', function(e) { handleMerchantDelete(e); }); }); // Update close button handlers in createMerchantListUI document.getElementById('closeMerchantListButton').addEventListener('click', (e) => { e.stopPropagation(); // Prevent event bubbling closeActiveSubUI(); }); } // Wörterliste erstellen 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 => `
${word}
`).join(''); wordsListDiv.innerHTML = `

Ausgeblendete Wörter (${currentWords.length})

${wordsListHTML}
`; // 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(); let visibleCount = 0; // Hole aktuelle Wörter statt die ursprüngliche Liste zu verwenden const currentWords = loadExcludeWords(); const totalCount = currentWords.length; document.querySelectorAll('.word-item').forEach(item => { const word = item.querySelector('span').textContent.toLowerCase(); const isVisible = word.includes(searchTerm); item.style.display = isVisible ? 'flex' : 'none'; if (isVisible) visibleCount++; }); // Update heading counter const heading = wordsListDiv.querySelector('h4'); if (heading) { heading.textContent = searchTerm ? `Ausgeblendete Wörter (${visibleCount}/${totalCount})` : `Ausgeblendete Wörter (${totalCount})`; } }); // Alle Wörter entfernen Button 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(); // Immediately update counter in heading const heading = wordsListDiv.querySelector('h4'); if (heading) { heading.textContent = 'Ausgeblendete Wörter (0)'; } } }); // Add delete handlers document.querySelectorAll('.delete-word').forEach(button => { button.addEventListener('click', function(e) { handleWordDelete(e); }); }); // Update close button handlers in createExcludeWordsUI document.getElementById('closeWordsListButton').addEventListener('click', (e) => { e.stopPropagation(); // Prevent event bubbling closeActiveSubUI(); }); } // Liste der Händler/Wörter aktualisieren function updateActiveLists() { const colors = getThemeColors(); if (activeSubUI === 'merchant' && merchantListDiv) { const merchantList = document.getElementById('merchantList'); if (merchantList) { const currentMerchants = loadExcludeMerchants(); merchantList.innerHTML = currentMerchants.map(merchant => `
${merchant.name} ID: ${merchant.id}
`).join(''); // Event Listener neu hinzufügen document.querySelectorAll('.delete-merchant').forEach(button => { button.addEventListener('click', function(e) { handleMerchantDelete(e); }); }); } } else if (activeSubUI === 'words' && wordsListDiv) { const wordsList = document.getElementById('wordsList'); if (wordsList) { const currentWords = loadExcludeWords(); wordsList.innerHTML = currentWords.map(word => `
${word}
`).join(''); // Event Listener neu hinzufügen document.querySelectorAll('.delete-word').forEach(button => { button.addEventListener('click', handleWordDelete); }); } } } // UI-Komponenten // Wort zur Liste hinzufügen 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 = ` ${word} `; // Insert at beginning of list wordsList.insertBefore(div, wordsList.firstChild); } // Händler zur Liste hinzufügen 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 = ` ${merchant.name} `; // Insert at beginning of list merchantList.insertBefore(div, merchantList.firstChild); } 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; -webkit-overflow-scrolling: touch; /* Für besseres Scrolling auf iOS */ `; // Add touch event handlers for mobile scrolling if (IS_TOUCH_DEVICE) { let touchStartY = 0; let scrollStartY = 0; wordSuggestionList.addEventListener('touchstart', (e) => { touchStartY = e.touches[0].pageY; scrollStartY = wordSuggestionList.scrollTop; // Verhindern dass der Touch-Event die Liste schließt e.stopPropagation(); }, { passive: true }); wordSuggestionList.addEventListener('touchmove', (e) => { const touchY = e.touches[0].pageY; const deltaY = touchStartY - touchY; wordSuggestionList.scrollTop = scrollStartY + deltaY; // Verhindern dass die Seite scrollt während in der Liste gescrollt wird if (wordSuggestionList.scrollHeight > wordSuggestionList.clientHeight) { const isAtTop = wordSuggestionList.scrollTop === 0; const isAtBottom = wordSuggestionList.scrollTop + wordSuggestionList.clientHeight >= wordSuggestionList.scrollHeight; if ((isAtTop && deltaY < 0) || (isAtBottom && deltaY > 0)) { e.preventDefault(); } } }, { passive: false }); wordSuggestionList.addEventListener('touchend', (e) => { e.stopPropagation(); }, { passive: true }); } // Rest of the function stays the same wordSuggestionList.innerHTML = suggestedWords .map(word => `
${word}
`).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); }); // Update position on scroll/resize 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); // 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(); }; } // UI-Styling 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; }); } }); } // 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 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%); `; } // Aktive Sub-UIs schließen function closeActiveSubUI() { if (activeSubUI === 'merchant') { merchantListDiv?.remove(); const btn = document.getElementById('showMerchantListButton'); if (btn) { btn.innerHTML = ' Händlerfilter verwalten'; btn.removeAttribute('data-processing'); } } else if (activeSubUI === 'words') { wordsListDiv?.remove(); const btn = document.getElementById('showWordsListButton'); if (btn) { btn.innerHTML = ' Wortfilter verwalten'; } } activeSubUI = null; } // --- 6. Event Handler --- // Klick-Handler außerhalb der UI 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); } // Wort-Auswahl Handler 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; } } // Händler-Löschung Handler function handleMerchantDelete(e) { e.preventDefault(); e.stopPropagation(); const deleteButton = e.target.closest('.delete-merchant'); if (!deleteButton) return; const idToDelete = deleteButton.dataset.id; const merchantItem = deleteButton.closest('.merchant-item'); // Update merchants array const merchantsData = loadExcludeMerchants(); const updatedMerchants = merchantsData.filter(m => m.id !== idToDelete); saveExcludeMerchants(updatedMerchants); // Get search state and counts const searchInput = document.getElementById('merchantSearch'); const searchTerm = searchInput?.value.trim().toLowerCase(); const totalItems = document.querySelectorAll('.merchant-item').length; // Calculate visible items for search let visibleCount = 0; if (searchTerm) { const visibleItems = Array.from(document.querySelectorAll('.merchant-item')).filter(item => { const merchantName = item.querySelector('span').textContent.toLowerCase(); const isVisible = merchantName.includes(searchTerm) && item.querySelector('.delete-merchant').dataset.id !== idToDelete; return isVisible; }); visibleCount = visibleItems.length; } // Remove item and update UI merchantItem.remove(); processArticles(); // Update counter in heading const heading = merchantListDiv.querySelector('h4'); if (heading) { const newTotal = totalItems - 1; heading.textContent = searchTerm ? `Ausgeblendete Händler (${visibleCount}/${newTotal})` : `Ausgeblendete Händler (${newTotal})`; } } function handleWordDelete(e) { e.preventDefault(); e.stopPropagation(); const deleteButton = e.target.closest('.delete-word'); if (!deleteButton) return; const wordToDelete = deleteButton.dataset.word; const wordItem = deleteButton.closest('.word-item'); // Update excludeWords array excludeWords = excludeWords.filter(word => word !== wordToDelete); saveExcludeWords(excludeWords); // Get search state and counts const searchInput = document.getElementById('wordSearch'); const searchTerm = searchInput?.value.trim().toLowerCase(); const totalItems = document.querySelectorAll('.word-item').length; // Calculate visible items for search let visibleCount = 0; if (searchTerm) { const visibleItems = Array.from(document.querySelectorAll('.word-item')).filter(item => { const itemWord = item.querySelector('span').textContent.toLowerCase(); const isVisible = itemWord.includes(searchTerm) && itemWord !== wordToDelete.toLowerCase(); return isVisible; }); visibleCount = visibleItems.length; } // Remove item and update UI wordItem.remove(); processArticles(); // Update counter in heading const heading = wordsListDiv.querySelector('h4'); if (heading) { const newTotal = totalItems - 1; heading.textContent = searchTerm ? `Ausgeblendete Wörter (${visibleCount}/${newTotal})` : `Ausgeblendete Wörter (${newTotal})`; } } // 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; } // --- 7. Button-Management --- // Button-Funktionen 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 = ` `; // 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; }; }); } 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'); }); } 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 = ''; 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); } // --- 8. Filter-System --- // Filter-Funktionalität function injectMaxPriceFilter() { const filterForm = document.querySelector('.subNavMenu-list form:first-of-type ul'); if (!filterForm) return; // Get theme colors const colors = getThemeColors(); const isDark = isDarkMode(); // Create list items for the filters const filterItems = document.createElement('li'); filterItems.innerHTML = `
Maximalpreis filtern
`; // Insert at the beginning of the filter form filterForm.appendChild(filterItems); // Add event listeners const coldDealsToggle = document.getElementById('hideColdDealsToggle'); if (coldDealsToggle) { coldDealsToggle.addEventListener('change', (e) => { hideColdDeals = e.target.checked; GM_setValue('hideColdDeals', hideColdDeals); localStorage.setItem(HIDE_COLD_DEALS_KEY, hideColdDeals); processArticles(); }); } const priceInput = document.getElementById('maxPriceFilterInput'); if (priceInput) { const formatPrice = (value) => { let cleaned = value.replace(/[^\d.,]/g, ''); const parts = cleaned.split(','); if (parts.length > 2) { cleaned = parts.slice(0, -1).join('') + ',' + parts.slice(-1); } if (parts.length === 2) { const intPart = parts[0].replace(/\./g, ''); return Number(intPart).toLocaleString('de-DE') + ',' + parts[1]; } else { const intPart = cleaned.replace(/\./g, ''); return Number(intPart).toLocaleString('de-DE'); } }; // Prevent menu from closing on mobile keyboard open if (IS_TOUCH_DEVICE) { const subNavMenu = document.querySelector('.subNavMenu'); priceInput.addEventListener('focus', () => { // Add a class to prevent auto-close subNavMenu?.classList.add('keep-open'); // Select all text priceInput.select(); // Prevent any existing scroll handlers from closing the menu const preventClose = (e) => { if (document.activeElement === priceInput) { e.stopPropagation(); } }; // Capture phase to intercept before other handlers window.addEventListener('scroll', preventClose, true); // Cleanup on blur const cleanup = () => { subNavMenu?.classList.remove('keep-open'); window.removeEventListener('scroll', preventClose, true); priceInput.removeEventListener('blur', cleanup); }; priceInput.addEventListener('blur', cleanup, { once: true }); }); } else { // Desktop focus handler priceInput.addEventListener('focus', () => { priceInput.select(); }); } priceInput.addEventListener('input', (e) => { e.stopPropagation(); e.target.value = formatPrice(e.target.value); }); priceInput.addEventListener('blur', (e) => { const value = e.target.value; const numStr = value.replace(/\./g, '').replace(',', '.'); const numericValue = parseFloat(numStr); if (!isNaN(numericValue) && numericValue >= 0) { saveMaxPrice(numericValue); processArticles(); } }); } } // --- 9. Backup und Wiederherstellung --- // Backup-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) { alert('Fehler beim Erstellen des Backups: ' + error.message); } } // Backup wiederherstellen 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); // Lade aktuelle Daten const currentWords = new Set(loadExcludeWords()); const currentMerchants = new Map( loadExcludeMerchants().map(m => [m.id, m]) ); // Merge Wörter (Duplikate werden automatisch entfernt) restoredData.excludeWords.forEach(word => currentWords.add(word)); const mergedWords = Array.from(currentWords); // Merge Händler (bei gleicher ID behält der existierende Eintrag Vorrang) restoredData.merchantsData.forEach(merchant => { if (!currentMerchants.has(merchant.id)) { currentMerchants.set(merchant.id, merchant); } }); const mergedMerchants = Array.from(currentMerchants.values()); // Speichere zusammengeführte Daten GM_setValue('excludeWords', mergedWords); localStorage.setItem('excludeWords', JSON.stringify(mergedWords)); excludeWords = mergedWords; saveExcludeMerchants(mergedMerchants); // Behalte existierende Einstellungen wenn vorhanden if (typeof restoredData.maxPrice === 'number' && maxPrice === 0) { saveMaxPrice(restoredData.maxPrice); } if (typeof restoredData.hideColdDeals === 'boolean' && !hideColdDeals) { hideColdDeals = restoredData.hideColdDeals; GM_setValue('hideColdDeals', hideColdDeals); localStorage.setItem('hideColdDeals', hideColdDeals); } if (isSettingsOpen) { updateUITheme(); } processArticles(); alert('Backup wurde erfolgreich wiederhergestellt.'); } catch (error) { alert('Fehler beim Wiederherstellen des Backups: ' + error.message); } }; reader.readAsText(file); } // --- 10. Hilfsfunktionen --- // HTML dekodieren function decodeHtml(html) { const txt = document.createElement('textarea'); txt.innerHTML = html; return txt.value; } 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' ? ' Händlerfilter verwalten' : ' 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(); } function resetUIState() { isSettingsOpen = false; activeSubUI = null; dealThatOpenedSettings = null; suggestedWords = []; settingsDiv?.remove(); merchantListDiv?.remove(); wordsListDiv?.remove(); } // --- 11. Initialisierung --- // Startup function init() { // Daten synchronisieren syncStorage(); excludeWords = loadExcludeWords(); // UI initialisieren initializeUI(); // Observer starten initObserver(); } function initializeUI() { // Initial UI Setup processArticles(); addSettingsButton(); addHideButtons(); addMerchantPageHideButton(); // Initialize filter observer filterObserver.observe(document.body, { childList: true, subtree: true }); // Add filters if menu already exists const filterMenu = document.querySelector('.subNavMenu-list'); if (filterMenu) { injectMaxPriceFilter(); } } // Observer Initialisierung function initObserver() { observer.disconnect(); observer.observe(document.body, { childList: true, subtree: true }); // Sofortige Verarbeitung requestAnimationFrame(() => { processArticles(); addSettingsButton(); addHideButtons(); }); } // Start script - nach DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // Aufräumen bestehender Daten beim Skriptstart (function cleanupMerchantData() { const merchants = loadExcludeMerchants(); saveExcludeMerchants(merchants); })();