// ==UserScript== // @name mydealz Manager // @namespace http://tampermonkey.net/ // @version 1.14.0 // @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 // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @downloadURL none // ==/UserScript== // Versions-Änderungen // //#region --- 1. Initialisierung und Grundeinstellungen --- // ===== Konstanten und Konfiguration ===== // --- Storage Keys --- const VERSION_PREFIX = 'mdm_version_'; const HIDDEN_DEALS_KEY = 'hiddenDeals'; const HIDE_COLD_DEALS_KEY = 'hideColdDeals'; const MAX_PRICE_KEY = 'maxPrice'; const LAST_HIDDEN_DEAL_SHOWN = 'lastHiddenDealShown'; const PREFERRED_SORT_KEY = 'mydealz_preferred_sort'; // --- Selektoren --- const ARTICLE_SELECTOR = '.thread--deal, .thread--type--list'; const MERCHANT_PAGE_SELECTOR = '.merchant-banner'; // --- System/Performance Konstanten --- const CLEANUP_TIME = 30000; const IS_TOUCH_DEVICE = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0); const DEBUG = false; // --- Feature Flags --- const FEATURES = { hideMatchingMerchantNames: 'hideMatchingMerchantNames', hideShareButtons: 'hideShareButtons', rememberSort: 'rememberSort' }; // ===== Instanzerkennung und Cleanup ===== (function detectMultipleInstances() { const currentVersion = GM_info.script.version; const now = Date.now(); try { // Setze Marker für diese Version const myKey = VERSION_PREFIX + currentVersion; localStorage.setItem(myKey, now.toString()); // Prüfe auf alle aktiven Versionen const activeVersions = new Set(); for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (!key?.startsWith(VERSION_PREFIX)) continue; const version = key.replace(VERSION_PREFIX, ''); const timestamp = parseInt(localStorage.getItem(key) || '0'); if ((now - timestamp) < CLEANUP_TIME) { activeVersions.add(version); } } if (activeVersions.size > 1) { const warningMsg = `⚠️ Warnung: Es wurden mehrere Versionen des mydealz Managers gefunden!\n\nAktive Versionen:\n${Array.from(activeVersions).join('\n')}\n\nBitte deaktiviere alle Versionen bis auf eine in deinem Script-Manager.`; alert(warningMsg); } } catch (e) { console.error('Error in instance detection:', e); } })(); // Cleanup beim Entladen der Seite window.addEventListener('unload', () => { try { localStorage.removeItem(VERSION_PREFIX + GM_info.script.version); } catch (e) { // Ignoriere Fehler beim Cleanup } }); // ===== UI-Ressourcen ===== // --- Font Awesome Einbindung --- 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); // --- Style Elemente --- const preventAutoCloseStyle = document.createElement('style'); preventAutoCloseStyle.textContent = ``; document.head.appendChild(preventAutoCloseStyle); // ===== UI-Konfiguration ===== // --- Titel-Speicher --- const ORIGINAL_TITLES = new Map(); // --- Sidebar Elemente --- const SIDEBAR_ELEMENTS = { banners: { headerBanners: { name: "Banner im Header", selector: '.messages', storageKey: 'hideHeaderBanners', hidden: false }, feedBanners: { name: "Banner im Feed", selector: '[id^="customBannerList-id-"], #eventBannerPortal', storageKey: 'hideFeedBanners', hidden: false } }, widgets: { topDiscussions: { name: "Widgets", selector: '.card.card--type-vertical.listLayout-box.aGrid.card--responsive', storageKey: 'hideTopDiscussionsWidget', hidden: false } } }; // ===== Observer-Konfiguration ===== const observer = new MutationObserver(throttle(() => { processArticles(); addSettingsButton(); addHideButtons(); }, 250)); // ===== UI-Zustand ===== // --- Hauptfenster --- let isSettingsOpen = false; let activeSubUI = null; let dealThatOpenedSettings = null; // --- UI-Elemente --- let settingsDiv = null; let merchantListDiv = null; let wordsListDiv = null; let uiClickOutsideHandler = null; // ===== Filter-Zustand ===== // --- Ausschlusslisten --- let excludeWords = []; let excludeMerchantIDs = []; let hiddenDeals = []; // --- Filter-Einstellungen --- let hideColdDeals = localStorage.getItem(HIDE_COLD_DEALS_KEY) === 'true'; let maxPrice = parseFloat(localStorage.getItem(MAX_PRICE_KEY)) || 0; // ===== Temporäre Daten ===== // --- Vorschläge --- let suggestedWords = []; let suggestionClickHandler = null; // --- Letzte Aktionen --- let lastHiddenDeal = null; // Speichert nur den letzten ausgeblendeten Deal let lastHiddenDealShown = false; // Wurde der letzte ausgeblendete Deal bereits angezeigt? // ===== Menü-Commands ===== // --- Command IDs --- let menuCommandId; let merchantCommandId; let backupCommandId; let restoreCommandId; // --- Feature-Flags --- let hideMatchingMerchantNames = GM_getValue('hideMatchingMerchantNames', false); let hideCustomBanners = GM_getValue('hideCustomBanners', false); let hideShareButtons = GM_getValue('hideShareButtons', false); // --- Temporäre Listen --- let recentHiddenDeals = []; //#endregion //#region --- 2. Hilfsfunktionen (Utility Functions) --- // ===== HTML & Text Verarbeitung ===== // HTML dekodieren function decodeHtml(html) { const txt = document.createElement('textarea'); txt.innerHTML = html; return txt.value; } // Regex-Sonderzeichen escapen function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // ===== Performance Optimierung ===== // Funktion zur Begrenzung der Ausführungshäufigkeit (Throttling) function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => { inThrottle = false; return false; }, limit); } } } // Liefert Theme-spezifische Farben basierend auf aktuellem Theme function getThemeColors() { // Theme-Erkennung inline const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const htmlElement = document.documentElement; const bodyElement = document.body; 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')); // Direkt die entsprechenden Farben zurückgeben return isDark ? THEME_COLORS.dark : THEME_COLORS.light; } // === Text-Analyse === // Wörter aus Deal-Titel extrahieren function getWordsFromTitle(deal) { const titleElement = deal.querySelector('.thread-title a'); 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', 'o.', // oder 'z.B.', 'z.b.', // zum Beispiel 'inkl.', // inklusive 'max.', // maximal 'min.', // minimal 'ca.', // circa 'vs.', // versus 'eff.', // effektiv 'mtl.', // monatlich 'bzw.', // beziehungsweise 'evtl.', // eventuell 'uvm.', // und vieles mehr 'etc.', // et cetera 'zzgl.', // zuzüglich 'Nr.', 'nr.', // Nummer 'St.', 'st.', // Stück 'usw.', // und so weiter 'u.a.', // unter anderem 'u.U.', // unter Umständen 'ggf.', // gegebenenfalls 'p.', // pro/per ]; const ignoreChars = ['&', '+', '!', '-', '/', '%', '–']; const units = ['MB/s', 'GB/s', 'KB/s', 'Mbit/s', 'Gbit/s', 'Kbit/s']; const priceContextWords = ['effektiv']; const specialBrands = ['RTL+']; 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) => { // Check for special brands first if (specialBrands.includes(word)) { return word; } // Rest of the existing cleanWord function if (units.some(unit => word.includes(unit))) { 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 und Abkürzungen schützen let tempTitle = title; const replacements = new Map(); // Erst die Einheiten schützen units.forEach((unit, index) => { const placeholder = `__UNIT${index}__`; while (tempTitle.includes(unit)) { tempTitle = tempTitle.replace(unit, placeholder); replacements.set(placeholder, unit); } }); // Dann die Abkürzungen schützen (z.B. "o." als ganzes Wort) ignoreWords.forEach((word, index) => { if (word.includes('.')) { const placeholder = `__ABBR${index}__`; // Verbesserte Regex für Abkürzungen, die auch Zahlen berücksichtigt const regex = new RegExp(`\\b${word.replace('.', '\\.')}\\s*(?=\\d|\\s|$)`, 'g'); while (regex.test(tempTitle)) { tempTitle = tempTitle.replace(regex, (match) => { const replacement = ' '; // Ersetze Abkürzung durch Leerzeichen return replacement; }); } } }); // Split und Platzhalter wiederherstellen return tempTitle .split(/[\s\/]+/) .map(word => { replacements.forEach((original, placeholder) => { if (word.includes(placeholder)) { word = word.replace(placeholder, original); } }); return word; }) .filter(word => word.length > 0); // Entferne leere Strings }; return splitTitle(rawTitle) .map(cleanWord) .filter(shouldKeepWord) .filter((word, index, self) => self.indexOf(word) === index); } //#endregion //#region --- 3. Datenverwaltung --- // ===== Einstellungen laden/speichern ===== // Laden aller gespeicherten Einstellungen function loadSettings() { // Lade Wortfilter und Händlerfilter excludeWords = loadExcludeWords(); const merchantsData = loadExcludeMerchants(); excludeMerchantIDs = merchantsData.map(m => m.id); // Lade Preisfilter maxPrice = parseFloat(GM_getValue('maxPrice', 0)) || 0; // Lade UI-Einstellungen hideColdDeals = GM_getValue('hideColdDeals', false); hideMatchingMerchantNames = GM_getValue('hideMatchingMerchantNames', false); window.hideMatchingMerchantNames = hideMatchingMerchantNames; // Lade Banner-Einstellung und aktualisiere basierend auf individuellen Einstellungen hideCustomBanners = GM_getValue('hideCustomBanners', false); // Wenn hideCustomBanners nicht explizit aktiviert ist, prüfe ob alle individuellen // Elemente ausgeblendet sind und setze hideCustomBanners entsprechend if (!hideCustomBanners) { const allHidden = Object.values(SIDEBAR_ELEMENTS).every(element => element.hidden); if (allHidden && Object.values(SIDEBAR_ELEMENTS).length > 0) { hideCustomBanners = true; } } window.hideCustomBanners = hideCustomBanners; // Lade Share Button Einstellung hideShareButtons = GM_getValue('hideShareButtons', false); window.hideShareButtons = hideShareButtons; // Lade versteckte Deals hiddenDeals = GM_getValue('hiddenDeals', []); // Lade letzten versteckten Deal lastHiddenDeal = GM_getValue('lastHiddenDeal', null); lastHiddenDealShown = GM_getValue(LAST_HIDDEN_DEAL_SHOWN, false); // Wenn lastHiddenDeal null ist, setze auch lastHiddenDealShown auf true if (!lastHiddenDeal) { lastHiddenDealShown = true; GM_setValue(LAST_HIDDEN_DEAL_SHOWN, true); } // Lade Banner-Status Object.keys(SIDEBAR_ELEMENTS.banners).forEach(key => { const element = SIDEBAR_ELEMENTS.banners[key]; element.hidden = GM_getValue(element.storageKey, false); // Sofort Sichtbarkeit anwenden if (element.hidden) { document.querySelectorAll(element.selector).forEach(el => { el.style.display = 'none'; }); } }); // Lade Sortierungsspeicher-Einstellung window.rememberSort = GM_getValue('rememberSort', true); const applyWidgetVisibilityOnce = () => { Object.keys(SIDEBAR_ELEMENTS.widgets).forEach(key => { const element = SIDEBAR_ELEMENTS.widgets[key]; element.hidden = GM_getValue(element.storageKey, false); if (element.hidden) { document.querySelectorAll(element.selector).forEach(el => { el.style.display = 'none'; }); } }); }; // Einmalig ausführen mit Verzögerung für dynamische Elemente setTimeout(applyWidgetVisibilityOnce, 500); // UI Updates in einem requestAnimationFrame bündeln requestAnimationFrame(() => { updateCustomBannerIcon(); updateSidebarElementsUI(); }); } const combinedObserver = new MutationObserver(throttle(() => { // Prozessiere Artikel und UI processArticles(); addSettingsButton(); addHideButtons(); // Prüfe Widget-Sichtbarkeit Object.keys(SIDEBAR_ELEMENTS.widgets).forEach(key => { const element = SIDEBAR_ELEMENTS.widgets[key]; if (element.hidden) { document.querySelectorAll(element.selector).forEach(el => { el.style.display = 'none'; }); } }); }, 250)); // Observer starten combinedObserver.observe(document.body, { childList: true, subtree: true }); // Storage-Synchronisation zwischen GM und localStorage 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; } // rememberSort const gmRememberSort = GM_getValue('rememberSort', null); const lsRememberSort = localStorage.getItem('rememberSort') === 'true'; const effectiveRememberSort = gmRememberSort !== null ? gmRememberSort : lsRememberSort; GM_setValue('rememberSort', effectiveRememberSort); localStorage.setItem('rememberSort', effectiveRememberSort.toString()); window.rememberSort = effectiveRememberSort; // hideMatchingMerchantNames const gmHideMatchingMerchantNames = GM_getValue('hideMatchingMerchantNames', null); const lsHideMatchingMerchantNames = localStorage.getItem('hideMatchingMerchantNames') === 'true'; const effectiveHideMatchingMerchantNames = gmHideMatchingMerchantNames !== null ? gmHideMatchingMerchantNames : lsHideMatchingMerchantNames; GM_setValue('hideMatchingMerchantNames', effectiveHideMatchingMerchantNames); localStorage.setItem('hideMatchingMerchantNames', effectiveHideMatchingMerchantNames.toString()); window.hideMatchingMerchantNames = effectiveHideMatchingMerchantNames; // hideShareButtons const gmHideShareButtons = GM_getValue('hideShareButtons', null); const lsHideShareButtons = localStorage.getItem('hideShareButtons') === 'true'; const effectiveHideShareButtons = gmHideShareButtons !== null ? gmHideShareButtons : lsHideShareButtons; GM_setValue('hideShareButtons', effectiveHideShareButtons); localStorage.setItem('hideShareButtons', effectiveHideShareButtons.toString()); window.hideShareButtons = effectiveHideShareButtons; // hideCustomBanners const gmHideCustomBanners = GM_getValue('hideCustomBanners', null); const lsHideCustomBanners = localStorage.getItem('hideCustomBanners') === 'true'; const effectiveHideCustomBanners = gmHideCustomBanners !== null ? gmHideCustomBanners : lsHideCustomBanners; GM_setValue('hideCustomBanners', effectiveHideCustomBanners); localStorage.setItem('hideCustomBanners', effectiveHideCustomBanners.toString()); window.hideCustomBanners = effectiveHideCustomBanners; // Sidebar Elements (Banner & Widgets) Object.entries(SIDEBAR_ELEMENTS).forEach(([category, items]) => { Object.entries(items).forEach(([key, element]) => { const storageKey = element.storageKey; const gmValue = GM_getValue(storageKey, null); const lsValue = localStorage.getItem(storageKey) === 'true'; const effectiveValue = gmValue !== null ? gmValue : lsValue; GM_setValue(storageKey, effectiveValue); localStorage.setItem(storageKey, effectiveValue.toString()); element.hidden = effectiveValue; }); }); migrationPerformed = true; } // Markiere Migration als abgeschlossen nur wenn tatsächlich Daten migriert wurden if (migrationPerformed) { GM_setValue('migrationComplete', true); } } // ===== Wortfilter-Verwaltung ===== // Speichern von Wortfiltern 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)); } // Laden von Wortfiltern 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; } // ===== Deal-Verwaltung ===== // Speichern ausgeblendeter Deals function saveHiddenDeals() { GM_setValue('hiddenDeals', hiddenDeals); localStorage.setItem('hiddenDeals', JSON.stringify(hiddenDeals)); } // ===== Händler-Verwaltung ===== // Speichern von Händlerfiltern 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; } // Laden von Händlerfiltern 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; } // ===== Preis-Verwaltung ===== // Speichern des Maximalpreises function saveMaxPrice(price) { // Convert to number if it's a string if (typeof price === 'string') { price = parseFloat(price.replace(',', '.')) || 0; } GM_setValue('maxPrice', price); localStorage.setItem('maxPrice', price.toString()); maxPrice = price; } //#endregion //#region --- 4. UI-System --- // ===== Basis UI-Funktionen ===== // Container, Styles, Theme function initUIContainers() { settingsDiv = document.createElement('div'); merchantListDiv = document.createElement('div'); wordsListDiv = document.createElement('div'); } 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; }); } }); } // === UI-Komponenten === // --- Dialog-Erstellung --- function createSettingsUI() { if (isSettingsOpen) return; isSettingsOpen = true; // Lade versteckte Deals neu const oldHiddenDeals = [...hiddenDeals]; // Copy old state hiddenDeals = GM_getValue('hiddenDeals', []); // Lade versteckte Deals neu hiddenDeals = GM_getValue('hiddenDeals', []); // Konsistente Überprüfung des "already shown" Status const wasAlreadyShown = GM_getValue(LAST_HIDDEN_DEAL_SHOWN, false); // Wenn bereits angezeigt oder kein letzter Deal vorhanden, dann nicht mehr anzeigen if (!lastHiddenDeal || wasAlreadyShown) { lastHiddenDealShown = true; GM_setValue(LAST_HIDDEN_DEAL_SHOWN, true); } else { lastHiddenDealShown = false; // GM_setValue nicht hier setzen, erst beim Schließen des UI } // 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%); width: 300px; height: 300px; /* Further reduced from 380px to 370px */ max-width: 90vw; max-height: 90vh; background: ${colors.background}; border: 1px solid ${colors.border}; border-radius: 5px; padding: 8px 15px; /* Reduced top/bottom padding from 10px to 8px */ z-index: 1000; color: ${colors.text}; display: flex; flex-direction: column; `; if (dealThatOpenedSettings) { const merchantLink = dealThatOpenedSettings.querySelector('a[data-t="merchantLink"]'); if (merchantLink) { merchantName = merchantLink.textContent.trim(); showMerchantButton = true; } } // Process articles when opening settings processArticles(); // Header const header = document.createElement('div'); header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; /* Reduced from 10px to 8px */ flex-shrink: 0; `; // Scrollbarer Content-Container const contentContainer = document.createElement('div'); contentContainer.style.cssText = ` flex-grow: 1; overflow-y: auto; margin-right: -5px; padding-right: 5px; margin-bottom: 5px; /* Reduced from 10px to 5px */ max-height: calc(300px - 60px); /* Wichtig: Ermöglicht Scrolling */ `; contentContainer.id = 'mdm-settings-content'; // ID hinzufügen für einfacheres Debugging // Accordion-Sektionen definieren und hinzufügen const sections = { quickActions: createAccordionSection('Schnellaktionen', 'bolt'), filter: createAccordionSection('Filter', 'filter'), features: createAccordionSection('Funktionen', 'toggle-on'), backup: createAccordionSection('Backup', 'save') }; // Quick Actions Section (standardmäßig offen) sections.quickActions.content.innerHTML = `
${IS_TOUCH_DEVICE ? ` ` : ''}
${showMerchantButton ? ` ` : ''} ${lastHiddenDeal && !lastHiddenDealShown ? `
` : ''}
`; // Filter Section sections.filter.content.innerHTML = `
Kalte Deals
`; // Vor dem Öffnen des UIs den Status der Icons aktualisieren const anyHidden = Object.values(SIDEBAR_ELEMENTS).some(element => element.hidden); const allHidden = Object.values(SIDEBAR_ELEMENTS).length > 0 && Object.values(SIDEBAR_ELEMENTS).every(element => element.hidden); // Passt hideCustomBanners basierend auf dem tatsächlichen Status der Elemente an if (allHidden) { window.hideCustomBanners = true; GM_setValue('hideCustomBanners', true); } else if (anyHidden) { // Bei teilweiser Sichtbarkeit soll hideCustomBanners deaktiviert sein window.hideCustomBanners = false; GM_setValue('hideCustomBanners', false); } else { window.hideCustomBanners = false; GM_setValue('hideCustomBanners', false); } // Aktualisiere die Icon-Klasse und Aria-Label für hideCustomBanners vor dem Hinzufügen const customBannerIconClass = allHidden ? 'fa-eye-slash' : anyHidden ? 'fa-eye-low-vision' : 'fa-eye'; const customBannerAriaLabel = allHidden ? 'Elemente versteckt' : anyHidden ? 'Einige Elemente versteckt' : 'Elemente sichtbar'; // Features Section sections.features.content.innerHTML = `
${document.querySelector('[data-t="shareBtn"]') ? `
Teilen Button
` : ''}
Händler im Titel
Sortierung merken
`; // Backup Section sections.backup.content.innerHTML = `
`; // Add sections to container Object.values(sections).forEach(({section}) => { contentContainer.appendChild(section); }); // Erste Sektion automatisch öffnen und andere schließen Object.values(sections).forEach(({section}, index) => { const header = section.querySelector('.accordion-header'); const content = section.querySelector('.accordion-content'); const icon = header.querySelector('.fa-chevron-down'); if (index === 0) { // Schnellaktionen content.style.display = 'block'; icon.style.transform = 'rotate(180deg)'; } else { content.style.display = 'none'; icon.style.transform = ''; } }); // Footer mit fixem Schließen-Button - weniger Abstand zum Content const footer = document.createElement('div'); footer.style.cssText = ` margin-top: 2px; /* Further reduced from 5px to 2px */ flex-shrink: 0; display: flex; justify-content: center; `; footer.innerHTML = ` `; // Zusammenbau der UI settingsDiv.appendChild(header); settingsDiv.appendChild(contentContainer); settingsDiv.appendChild(footer); document.body.appendChild(settingsDiv); // Nach dem Hinzufügen zum DOM den Button anpassen document.getElementById('closeSettingsButton').style.cssText = ` padding: 8px 16px; display: inline-block; width: auto; min-width: 100px; text-align: center; `; // Event-Listener für den Sortierung Button: document.getElementById('rememberSort')?.addEventListener('click', async (e) => { // Toggle state const newState = !window.rememberSort; window.rememberSort = newState; GM_setValue('rememberSort', newState); // Wenn Feature deaktiviert wurde, gespeicherte Sortierung entfernen if (!newState && localStorage.getItem(PREFERRED_SORT_KEY)) { localStorage.removeItem(PREFERRED_SORT_KEY); if (DEBUG) console.log('[MDM Sort] Gespeicherte Sortierung gelöscht (Feature deaktiviert)'); } // Update icon const icon = e.currentTarget.querySelector('i'); if (icon) { icon.className = `fas ${newState ? 'fa-toggle-on' : 'fa-toggle-off'}`; icon.setAttribute('aria-label', newState ? 'Sortierung wird gespeichert' : 'Sortierung wird nicht gespeichert'); } }); // Accordion section styling - reduce padding and margins const accordionSectionStyle = document.createElement('style'); accordionSectionStyle.textContent = ` .accordion-section { margin-bottom: 4px; /* Add a small gap between sections */ } .accordion-header { padding: 6px 8px !important; /* Reduced padding */ } .accordion-content { padding: 6px 8px !important; /* Reduced padding */ } `; document.head.appendChild(accordionSectionStyle); // Zusammenbau der UI settingsDiv.appendChild(header); settingsDiv.appendChild(contentContainer); settingsDiv.appendChild(footer); document.body.appendChild(settingsDiv); // Backup/Restore Buttons korrekt referenzieren const backupButton = document.getElementById('backupDataButton'); const restoreButton = document.getElementById('restoreDataButton'); const restoreFileInput = document.getElementById('restoreFileInput'); // Event Listener für den versteckten Datei-Input if (restoreFileInput) { restoreFileInput.addEventListener('change', (e) => { restoreData(e); }); } if (backupButton) { backupButton.addEventListener('click', () => { backupData(); }); } if (restoreButton) { restoreButton.addEventListener('click', () => { if (restoreFileInput) { restoreFileInput.click(); } else { } }); } // Add Word Button 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(); } }); } // Enable Keyboard Button (für Touch-Geräte) const enableKeyboardButton = document.getElementById('enableKeyboardButton'); if (enableKeyboardButton) { enableKeyboardButton.addEventListener('click', () => { const newWordInput = document.getElementById('newWordInput'); if (newWordInput) { newWordInput.readOnly = false; newWordInput.focus(); } }); } // Hide Merchant Button 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(); } } }); } // Show Words List Button const showWordsListButton = document.getElementById('showWordsListButton'); if (showWordsListButton) { showWordsListButton.addEventListener('click', () => { if (activeSubUI === 'words') { closeActiveSubUI(); } else { if (activeSubUI) closeActiveSubUI(); activeSubUI = 'words'; showWordsListButton.innerHTML = ' Wortfilter schließen'; createExcludeWordsUI(); } }); } // Show Merchant List Button const showMerchantListButton = document.getElementById('showMerchantListButton'); if (showMerchantListButton) { showMerchantListButton.addEventListener('click', () => { if (activeSubUI === 'merchant') { closeActiveSubUI(); } else { if (activeSubUI) closeActiveSubUI(); activeSubUI = 'merchant'; showMerchantListButton.innerHTML = ' Händlerfilter schließen'; createMerchantListUI(); } }); } // Add event listeners only if newWordInput exists const newWordInput = document.getElementById('newWordInput'); if (newWordInput) { // Enter Key Handler für das Eingabefeld newWordInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); const word = newWordInput.value.trim(); if (word && word.length > 0) { // Add to excludeWords if not already in the list if (!excludeWords.includes(word)) { excludeWords.push(word); saveExcludeWords(excludeWords); processArticles(); } // Clear the input field newWordInput.value = ''; // Close suggestion list document.getElementById('wordSuggestionList')?.remove(); } } }); // 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); cleanup(); }); // Add cleanup to window unload window.addEventListener('unload', cleanup); const maxPriceInput = document.getElementById('settingsMaxPrice'); // Korrekter ID if (maxPriceInput) { // Focus-Handler hinzufügen, der den Inhalt markiert maxPriceInput.addEventListener('focus', () => { maxPriceInput.select(); }); // Formatierer für die Eingabe const formatPrice = (value) => { let cleaned = value.replace(/[^\d.,]/g, ''); const parts = cleaned.split(','); // Begrenze Nachkommastellen auf 2 if (parts.length > 1) { parts[1] = parts[1].slice(0, 2); // Maximal 2 Stellen nach dem Komma cleaned = parts[0] + ',' + parts[1]; } if (parts.length > 2) { cleaned = parts.slice(0, -1).join('') + ',' + parts.slice(-1)[0].slice(0, 2); } 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'); } }; // Input-Handler hinzufügen für die Live-Formatierung maxPriceInput.addEventListener('input', (e) => { e.stopPropagation(); e.target.value = formatPrice(e.target.value); }); // Beim Verlassen des Feldes den Wert speichern maxPriceInput.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(); } }); // Behandlung für Touch-Geräte if (IS_TOUCH_DEVICE) { maxPriceInput.addEventListener('focus', () => { // Öffnet die numerische Tastatur auf Touch-Geräten maxPriceInput.setAttribute('inputmode', 'decimal'); // Markiert den Text für einfaches Überschreiben maxPriceInput.select(); }); } } // Get initial word suggestions suggestedWords = dealThatOpenedSettings ? getWordsFromTitle(dealThatOpenedSettings) : []; // Scroll-Handling einrichten const cleanupScrollHandling = setupScrollHandling(); // Cleanup erweitern const oldCleanup = cleanup; cleanup = () => { cleanupScrollHandling(); oldCleanup(); }; // Event-Listener für die "Zurück"-Buttons bei kürzlich ausgeblendeten Deals document.querySelector('.restore-deal-button')?.addEventListener('click', (e) => { const dealId = e.currentTarget.dataset.dealId; // Deal aus hiddenDeals entfernen hiddenDeals = hiddenDeals.filter(id => id !== dealId); saveHiddenDeals(); // Deal als angezeigt markieren lastHiddenDealShown = true; GM_setValue(LAST_HIDDEN_DEAL_SHOWN, true); // UI aktualisieren document.getElementById('lastHiddenDealSection').style.display = 'none'; // Finde den wiederhergestellten Deal const restoredDeal = document.getElementById(dealId); if (restoredDeal) { // Hide-Button Container zurücksetzen const hideButtonContainer = restoredDeal.querySelector('.cept-vote-temp div'); if (hideButtonContainer) { hideButtonContainer.style.display = 'none'; } } // Deals neu verarbeiten processArticles(); }); // Setup ClickOutsideHandler für das Settings-UI setupClickOutsideHandler(); document.getElementById('hideShareButtons')?.addEventListener('click', async (e) => { // Toggle state const newState = !window.hideShareButtons; window.hideShareButtons = newState; GM_setValue('hideShareButtons', newState); // Update icon const icon = e.currentTarget.querySelector('i'); if (icon) { icon.className = newState ? 'fas fa-eye-slash' : 'fas fa-eye'; icon.setAttribute('aria-label', newState ? 'Share Buttons versteckt' : 'Share Buttons sichtbar'); } // Update visibility updateShareButtonsVisibility(newState); await Promise.resolve(); processArticles(); }); document.getElementById('hideMatchingMerchantNames')?.addEventListener('click', async (e) => { // Toggle state const newState = !window.hideMatchingMerchantNames; window.hideMatchingMerchantNames = newState; GM_setValue('hideMatchingMerchantNames', newState); // Update icon const icon = e.currentTarget.querySelector('i'); if (icon) { icon.className = `fas ${newState ? 'fa-eye-slash' : 'fa-eye'}`; icon.setAttribute('aria-label', newState ? 'Händlernamen versteckt' : 'Händlernamen sichtbar'); } // Update visibility await Promise.resolve(); processArticles(); }); // Event Handler für den Custom Banner-Toggle document.getElementById('hideCustomBanners')?.addEventListener('click', async () => { // Toggle state const newState = !window.hideCustomBanners; // Aktualisiere alle SIDEBAR_ELEMENTS nur im Status Object.keys(SIDEBAR_ELEMENTS).forEach(key => { const element = SIDEBAR_ELEMENTS[key]; element.hidden = newState; GM_setValue(element.storageKey, newState); }); // Aktualisiere die Haupt-Variable window.hideCustomBanners = newState; GM_setValue('hideCustomBanners', newState); // Haupt-Icon aktualisieren updateCustomBannerIcon(); // WICHTIG: Aktualisiere auch die UI-Elemente in der Sidebar updateSidebarElementsUI(); }); document.getElementById('openSidebarElementsButton').addEventListener('click', (e) => { // Prüfen ob das UI bereits geöffnet ist const sidebarElementsDiv = document.getElementById('sidebarElementsDiv'); if (sidebarElementsDiv) { // UI ist bereits offen - schließen sidebarElementsDiv.remove(); } else { // UI ist geschlossen - öffnen createSidebarElementsUI(); } }); document.getElementById('hideColdDeals')?.addEventListener('click', async (e) => { // Toggle state hideColdDeals = !hideColdDeals; // Save to storage GM_setValue('hideColdDeals', hideColdDeals); localStorage.setItem(HIDE_COLD_DEALS_KEY, hideColdDeals.toString()); // Update icon const icon = e.currentTarget.querySelector('i'); if (icon) { icon.className = `fas ${hideColdDeals ? 'fa-eye-slash' : 'fa-eye'}`; icon.setAttribute('aria-label', hideColdDeals ? 'Kalte Deals versteckt' : 'Kalte Deals sichtbar'); } // Reprocess articles to apply the change await Promise.resolve(); processArticles(); }); } function createSidebarElementsUI() { const colors = getThemeColors(); const isMobile = window.innerWidth <= 768; const sidebarElementsDiv = document.createElement('div'); sidebarElementsDiv.id = 'sidebarElementsDiv'; sidebarElementsDiv.style.cssText = ` ${getSubUIPosition()} padding: 15px; background-color: ${colors.background}; border: 1px solid ${colors.border}; border-radius: 5px; width: 300px; color: ${colors.text}; z-index: 10003; box-shadow: 0 2px 8px rgba(0,0,0,0.1); `; // Gerätetyp erkennen für angepasste Bezeichnungen const sidebarText = isMobile ? "Feed" : "Seitenleiste"; let elementsHTML = ''; Object.entries({ banner: { title: "Banner", keys: ['headerBanners', 'feedBanners', 'voteBox'] }, widgets: { title: isMobile ? "Feed-Elemente" : "Widgets", keys: Object.keys(SIDEBAR_ELEMENTS.widgets) // Ändere dies um alle Widget-Keys einzuschließen } }).forEach(([catKey, category]) => { elementsHTML += ` `; }); sidebarElementsDiv.innerHTML = `
${elementsHTML}
`; document.body.appendChild(sidebarElementsDiv); // Event-Listener für die Toggle-Buttons document.querySelectorAll('.toggle-sidebar-element').forEach(button => { button.addEventListener('click', (e) => { const key = button.dataset.key; const type = button.dataset.type; const elementGroup = type === 'banner' ? SIDEBAR_ELEMENTS.banners : SIDEBAR_ELEMENTS.widgets; if (!key || !elementGroup[key]) return; const element = elementGroup[key]; const newState = !element.hidden; element.hidden = newState; GM_setValue(element.storageKey, newState); // Wende Sichtbarkeit auf DOM-Elemente an document.querySelectorAll(element.selector).forEach(el => { el.style.display = newState ? 'none' : ''; }); updateSidebarElementsUI(); updateCustomBannerIcon(); }); }); // Event-Listener für den Schließen-Button hinzufügen - nur dieses UI schließen document.getElementById('closeSidebarElementsButton').addEventListener('click', (e) => { e.stopPropagation(); // Verhindert Event-Bubbling const sidebarDiv = document.getElementById('sidebarElementsDiv'); if (sidebarDiv) { sidebarDiv.remove(); } }); } // Füge diese Funktion zum Togglen der Feed-Banner hinzu function toggleFeedBanners(hide) { const feedBannerConfig = SIDEBAR_ELEMENTS.banners.feedBanners; if (!feedBannerConfig) return; // Speichere Status feedBannerConfig.hidden = hide; GM_setValue(feedBannerConfig.storageKey, hide); // Wende Sichtbarkeit auf alle Feed-Banner an document.querySelectorAll('[id^="customBannerList-id-"]').forEach(banner => { banner.style.display = hide ? 'none' : ''; }); // Update Icon im UI updateSidebarElementsUI(); updateCustomBannerIcon(); } // Accordion-Sektion erstellen function createAccordionSection(title, iconName) { const section = document.createElement('div'); section.className = 'accordion-section'; const colors = getThemeColors(); section.innerHTML = `
${title}
`; // Hole Referenzen const header = section.querySelector('.accordion-header'); const content = section.querySelector('.accordion-content'); header.onclick = () => { // Schließe alle anderen Sektionen document.querySelectorAll('.accordion-section').forEach(otherSection => { if (otherSection !== section) { const otherContent = otherSection.querySelector('.accordion-content'); const otherIcon = otherSection.querySelector('.fa-chevron-down'); if (otherContent) { otherContent.style.display = 'none'; } if (otherIcon) { otherIcon.style.transform = ''; } } }); // Öffne/Schließe aktuelle Sektion const isOpen = content.style.display === 'block'; content.style.display = isOpen ? 'none' : 'block'; header.querySelector('.fa-chevron-down').style.transform = isOpen ? '' : 'rotate(180deg)'; }; return { section, content: section.querySelector('.accordion-inner') }; } // --- Filterlisten-Erstellung --- // 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; 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(); // Nach dem Hinzufügen zum DOM den Button anpassen document.getElementById('closeMerchantListButton').style.cssText = ` padding: 8px 16px; display: inline-block; width: auto; min-width: 100px; text-align: center; `; // 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; 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(); // Nach dem Hinzufügen zum DOM den Button anpassen document.getElementById('closeWordsListButton').style.cssText = ` padding: 8px 16px; display: inline-block; width: auto; min-width: 100px; text-align: center; `; // 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(); }); // Before adding to DOM document.body.appendChild(wordsListDiv); setupClickOutsideHandler(); } // === UI-Updates === // --- Listen & Status --- // 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); }); } } } 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 }); } 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(); }; } // Share-Buttons Ein-/Ausblenden function updateShareButtonsVisibility(hide) { const styleId = 'mdm-hide-share-buttons-style'; let styleElement = document.getElementById(styleId); if (hide) { if (!styleElement) { styleElement = document.createElement('style'); styleElement.id = styleId; styleElement.textContent = ` button[data-t="shareBtn"] { display: none !important; } `; document.head.appendChild(styleElement); } } else if (styleElement) { styleElement.remove(); } } // Custom Banner Icon aktualisieren function updateCustomBannerIcon() { // Suche das Icon-Element im DOM const iconElement = document.querySelector('#hideCustomBanners i'); if (!iconElement) return; // Prüfe Status der individuellen Elemente const anyHidden = Object.values(SIDEBAR_ELEMENTS).some(element => element.hidden); const allHidden = Object.values(SIDEBAR_ELEMENTS).length > 0 && Object.values(SIDEBAR_ELEMENTS).every(element => element.hidden); // Setze das Icon basierend auf dem Status if (allHidden) { // Wenn alle Elemente ausgeblendet sind, Auge durchgestrichen iconElement.className = 'fas fa-eye-slash'; iconElement.setAttribute('aria-label', 'Custom Banner versteckt'); } else if (anyHidden) { // Wenn einige Elemente ausgeblendet sind, Low-Vision Icon iconElement.className = 'fas fa-eye-low-vision'; iconElement.setAttribute('aria-label', 'Einige Banner versteckt'); } else { // Wenn alle Elemente sichtbar sind, normales Auge iconElement.className = 'fas fa-eye'; iconElement.setAttribute('aria-label', 'Custom Banner sichtbar'); } } // Sidebar-Elemente UI aktualisieren function updateSidebarElementsUI() { const sidebarElementsDiv = document.getElementById('sidebarElementsDiv'); if (!sidebarElementsDiv) return; document.querySelectorAll('.toggle-sidebar-element').forEach(button => { const key = button.dataset.key; const type = button.dataset.type; // Prüfe sowohl Banner als auch Widgets const elementGroup = type === 'banner' ? SIDEBAR_ELEMENTS.banners : SIDEBAR_ELEMENTS.widgets; if (!key || !elementGroup[key]) return; const element = elementGroup[key]; const icon = button.querySelector('i'); if (icon) { icon.className = `fas ${element.hidden ? 'fa-eye-slash' : 'fa-eye'}`; } }); Object.entries(SIDEBAR_ELEMENTS).forEach(([category, items]) => { Object.entries(items).forEach(([key, element]) => { }); }); } // === Event-Handler === // --- Click & Touch --- // 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') || e.target.closest('#openSidebarElementsButton') || e.target.closest('#closeSidebarElementsButton')) { return; } // Get current UI states const settingsOpen = settingsDiv?.parentNode; const merchantsOpen = merchantListDiv?.parentNode; const wordsOpen = wordsListDiv?.parentNode; const sidebarElementsOpen = document.getElementById('sidebarElementsDiv')?.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)) && (!sidebarElementsOpen || !document.getElementById('sidebarElementsDiv').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(); // Sicherer Zugriff mit optionaler Verkettung const sidebarDiv = document.getElementById('sidebarElementsDiv'); if (sidebarDiv?.parentNode) sidebarDiv.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); } 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; } 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; } } function setupScrollHandling() { let isScrollingUI = false; let lastActiveUI = null; let touchStartY = 0; // Hilfsfunktion zum Prüfen ob ein Element scrollbar ist const isScrollable = (element) => { return element.scrollHeight > element.clientHeight; }; // Hilfsfunktion zum Prüfen ob ein Element am Anfang/Ende des Scrollbereichs ist const isAtScrollLimit = (element, delta) => { if (delta > 0) { return element.scrollTop + element.clientHeight >= element.scrollHeight - 1; } else { return element.scrollTop <= 0; } }; function handleScroll(e) { // Prüfe ob der Mauszeiger über einem UI-Element ist const isOverUI = e.target.closest('#mdm-settings-popup, #merchantListDiv, #wordsListDiv, #wordSuggestionList, #sidebarElementsDiv') || settingsDiv?.contains(e.target) || merchantListDiv?.contains(e.target) || wordsListDiv?.contains(e.target) || document.getElementById('sidebarElementsDiv')?.contains(e.target); if (isOverUI) { // Verhindern des Standard-Scroll-Verhaltens (Website-Scrolling) e.preventDefault(); // Finde das scrollbare übergeordnete Element const scrollableContainer = e.target.closest('#mdm-settings-content, #merchantList, #wordsList, #wordSuggestionList, #sidebarElementsDiv'); if (scrollableContainer && isScrollable(scrollableContainer)) { // Manuelles Scrollen des Containers implementieren const deltaY = e.deltaY || e.detail || e.wheelDelta; const scrollAmount = deltaY > 0 ? 40 : -40; // Scroll-Schrittgröße // Scrolle den Container scrollableContainer.scrollTop += scrollAmount; } // Event-Propagation stoppen e.stopPropagation(); return false; } } // Event-Listener für das Mausrad mit passiver Option auf false (damit preventDefault funktioniert) document.addEventListener('wheel', handleScroll, { passive: false }); function handleTouchStart(e) { const touch = e.touches[0]; touchStartY = touch.clientY; const uiElements = [ settingsDiv, merchantListDiv, wordsListDiv, document.getElementById('wordSuggestionList'), document.getElementById('sidebarElementsDiv') ]; 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('#mdm-settings-content, #merchantList, #wordsList, #sidebarElementsDiv'); if (scrollableElement && isScrollable(scrollableElement)) { const deltaY = touchStartY - touch.clientY; // Immer preventDefault aufrufen, um Seiten-Scrolling zu verhindern e.preventDefault(); // Scrollen des UI-Elements scrollableElement.scrollTop += deltaY; } else { // Blockiere Scrollen außerhalb der Listen e.preventDefault(); } touchStartY = touch.clientY; } function handleMouseEnter() { isScrollingUI = true; lastActiveUI = this; } function handleMouseLeave() { isScrollingUI = false; lastActiveUI = null; } function setupUIElement(element) { if (!element?.parentNode) return; element.addEventListener('mouseenter', handleMouseEnter); element.addEventListener('mouseleave', handleMouseLeave); } function setupAllElements() { // Füge auch sidebarElementsDiv zu den zu überwachenden Elementen hinzu [ settingsDiv, merchantListDiv, wordsListDiv, document.getElementById('wordSuggestionList'), document.getElementById('sidebarElementsDiv') ].forEach(setupUIElement); } // Initial Setup setupAllElements(); // Event Listener if (IS_TOUCH_DEVICE) { document.addEventListener('touchstart', handleTouchStart, { passive: true }); document.addEventListener('touchmove', handleTouchMove, { passive: false }); } // MutationObserver für dynamisch hinzugefügte UIs const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.addedNodes.length) { // Prüfe auf neu hinzugefügtes Seitenleisten-UI const addedSidebarUI = Array.from(mutation.addedNodes).find( node => node.id === 'sidebarElementsDiv' ); if (addedSidebarUI) { setupUIElement(addedSidebarUI); } } }); 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'), document.getElementById('sidebarElementsDiv') ].forEach(el => { if (el?.parentNode) { el.removeEventListener('mouseenter', handleMouseEnter); el.removeEventListener('mouseleave', handleMouseLeave); } }); observer.disconnect(); }; } // --- Delete-Operationen --- 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})`; } } // 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})`; } } // === Layout & UI-Hilfen === // Sub-UI Position berechnen function getSubUIPosition() { const isTouch = IS_TOUCH_DEVICE; // Position relativ zum Manager-Popup berechnen const managerPopup = document.getElementById('mdm-settings-popup'); const popupRect = managerPopup?.getBoundingClientRect(); const position = isTouch ? ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10002; ` : ` position: fixed; top: ${popupRect ? `${popupRect.top + (popupRect.height / 2)}px` : '50%'}; left: ${popupRect ? (popupRect.right + 10) + 'px' : 'calc(50% + 310px)'}; transform: ${popupRect ? 'translateY(-50%)' : 'translate(-50%, -50%)'}; z-index: 10002; `; return position; } // 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 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; } //#endregion //#region --- 5. Deal-Verarbeitung --- // ===== Hauptfunktionen ===== // Artikel verarbeiten und filtern function processArticles() { // Am Anfang der Funktion den aktuellen Status aus Storage laden window.hideCustomBanners = GM_getValue('hideCustomBanners', false); const processedDeals = new Set(); const articles = document.querySelectorAll('article.thread--deal, article.thread--voucher'); let hiddenCount = 0; articles.forEach(article => { // Debug: Test if shouldExcludeArticle is being called const shouldExclude = shouldExcludeArticle(article); if (shouldExclude) { // FIX: Sicherstellen, dass die Anzeige wirklich auf 'none' gesetzt wird article.style.display = 'none'; article.setAttribute('data-hidden-by-mydealz-manager', 'true'); // Markieren für Debug-Zwecke hiddenCount++; } else { // Sicherstellen, dass der Artikel sichtbar ist article.style.display = ''; article.removeAttribute('data-hidden-by-mydealz-manager'); } }); document.querySelectorAll('article.thread--type-list').forEach(deal => { // Korrigierter Selektor für die Temperatur const tempElement = deal.querySelector('.cept-vote-temp span'); const temperatureText = tempElement ? tempElement.textContent.trim() : null; // Extrahiere nur die Zahl aus dem Text const temperatureMatch = temperatureText ? temperatureText.match(/([-+]?\d+)°/) : null; const temperature = temperatureMatch ? parseInt(temperatureMatch[1]) : null; const isCold = temperature !== null && temperature < 0; // Markiere das Element für bessere Sichtbarkeit im Debug deal.dataset.cold = isCold ? 'true' : 'false'; if (hideColdDeals && isCold) { deal.style.display = 'none'; } else { deal.style.display = ''; // Reset display } }); // Bestehende Deal-Verarbeitung 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'; // Custom Banner Handling if (window.hideCustomBanners) { Object.entries(SIDEBAR_ELEMENTS).forEach(([category, items]) => { Object.entries(items).forEach(([key, element]) => { const elements = document.querySelectorAll(element.selector); elements.forEach(el => { const oldDisplay = el.style.display; el.style.display = element.hidden ? 'none' : ''; }); }); }); } // Händlername im Titel if (window.hideMatchingMerchantNames) { const titleElement = deal.querySelector('.thread-title'); const merchantLink = deal.querySelector('a[data-t="merchantLink"]'); if (titleElement && merchantLink) { const merchantName = merchantLink.textContent.trim(); const titleLink = titleElement.querySelector('a'); if (titleLink) { // Speichere ursprünglichen Titel falls noch nicht geschehen if (!ORIGINAL_TITLES.has(dealId)) { ORIGINAL_TITLES.set(dealId, titleLink.textContent); } const originalTitle = ORIGINAL_TITLES.get(dealId) || titleLink.textContent; const newTitle = removeMerchantNameFromTitle(originalTitle, merchantName); titleLink.textContent = newTitle; } } } else { // Originaltitel wiederherstellen falls vorhanden const titleLink = deal.querySelector('.thread-title a'); const dealId = deal.getAttribute('id'); if (titleLink && ORIGINAL_TITLES.has(dealId)) { titleLink.textContent = ORIGINAL_TITLES.get(dealId); } } }); // Sortierungsspeicher verarbeiten processSortRemembering(); } // ===== Filterlogik ===== // 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; } } const dealId = article.getAttribute('id'); // Price check if (maxPrice > 0) { const priceSelectors = ['.thread-price', '.text--color-greyShade']; for (const selector of priceSelectors) { const priceElement = article.querySelector(selector); if (priceElement) { const priceText = priceElement.textContent.trim(); // Ignore percentage discounts if (priceText.includes('%')) { continue; } // Ignore negative values (e.g., "-20€") if (priceText.startsWith('-')) { continue; } // Check if the text contains a price in € format if (!priceText.includes('€')) { continue; } // Extract numeric price - handle European format (47.170,00€) const euroFormatMatch = priceText.match(/([\d.,]+)\s*€/); if (euroFormatMatch) { let extractedPrice = euroFormatMatch[1]; // Remove all dots first (thousand separators) extractedPrice = extractedPrice.replace(/\./g, ''); // Then replace comma with dot for decimal extractedPrice = extractedPrice.replace(',', '.'); // Hier ist die wichtige Änderung: const hinzufügen const priceValue = parseFloat(extractedPrice); if (!isNaN(priceValue) && priceValue > maxPrice) { return true; } } } } } // 3. Complex checks // Get title text const rawTitle = titleElement.querySelector('a')?.getAttribute('title') || titleElement.innerText; // Hilfsfunktion für ß zu ss und ue zu ü Konvertierung function normalizeGerman(text) { return text.toLowerCase() .replace(/ß/g, 'ss') .replace(/ue/g, 'ü'); } // Normalisiere den Titel const processedTitle = normalizeGerman(rawTitle); // Check excludeWords if (excludeWords.some(word => { const searchTerm = normalizeGerman(word); const lowerTitle = normalizeGerman(processedTitle); // Handle words in brackets like [CB] if (searchTerm.startsWith('[') && searchTerm.endsWith(']')) { const match = lowerTitle.includes(searchTerm); return match; } // Handle words with special characters (+) if (searchTerm.includes('+')) { const match = lowerTitle.includes(searchTerm); return match; } // Handle multi-word phrases if (searchTerm.includes(' ') || searchTerm.includes('-')) { const variations = [ searchTerm, // Only generate variations if the original search term has hyphens ...(searchTerm.includes('-') ? [ searchTerm.replace(/-/g, ' '), searchTerm.replace(/-/g, ''), searchTerm.replace(/-/g, ' ').trim(), ] : []) ]; const uniqueVariations = [...new Set(variations)]; return uniqueVariations.some(variant => { if (variant.includes(' ')) { const words = variant.split(' ').filter(w => w.length > 0); const firstWord = words[0]; const firstWordBoundaryRegex = new RegExp(`\\b${firstWord}\\b`, 'i'); if (!firstWordBoundaryRegex.test(lowerTitle)) { return false; } // First, check for matches where the words are all joined (like "DolbyAtmos") const joinedWords = words.join(''); if (lowerTitle.includes(joinedWords)) { return false; } // Check for hyphenated versions that might be part of longer terms const hyphenatedVariant = words.join('-'); if (lowerTitle.includes(hyphenatedVariant)) { // Find all occurrences and verify they're not part of longer hyphenated terms let isCompleteHyphenatedTerm = false; let startPos = 0; while (startPos < lowerTitle.length) { const foundPos = lowerTitle.indexOf(hyphenatedVariant, startPos); if (foundPos === -1) break; const endPos = foundPos + hyphenatedVariant.length; // Check if this is a complete term (not followed by another hyphen) if (endPos >= lowerTitle.length || lowerTitle[endPos] !== '-') { isCompleteHyphenatedTerm = true; break; } // Move to check next occurrence startPos = endPos; } // If we only found it as part of longer terms (like "Dolby-Atmos-Soundbar"), return false if (!isCompleteHyphenatedTerm) { return false; } } let lastIndex = -1; // Check for proper spacing between words return words.every(word => { const index = lowerTitle.indexOf(word, lastIndex + 1); if (index === -1) return false; // If not the first word, check if words are properly adjacent if (lastIndex !== -1) { const betweenText = lowerTitle.substring(lastIndex + words[words.indexOf(word) - 1].length, index); // Only allow whitespace, hyphens, or specific delimiters between words const allowedSeparators = /^[\s\-]*$/; if (!allowedSeparators.test(betweenText)) { return false; } } lastIndex = index; return true; }); } // For hyphenated variations, ensure exact match with proper boundaries if (variant.includes('-')) { // Split the hyphenated term to check if it's a complete standalone term const parts = variant.split('-'); // The regex should check for exact match of the variant, not as part of a longer hyphenated term const escapedVariant = variant.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); // Check if this is the exact hyphenated term, not part of a longer chain // This ensures "Dolby-Atmos" doesn't match within "Dolby-Atmos-Soundbar" if (lowerTitle.includes(variant)) { // Special case: If the search term itself is hyphenated (like "Dolby-Atmos"), // we want it to match even when part of longer hyphenated terms like "Dolby-Atmos-Soundbar" if (searchTerm.includes('-')) { return true; } // For converted terms (like "Dolby Atmos" converted to "Dolby-Atmos"), // we're stricter and don't want to match if part of longer hyphenated terms // Find all occurrences let startIndex = 0; let foundValid = false; while (startIndex < lowerTitle.length) { const foundIndex = lowerTitle.indexOf(variant, startIndex); if (foundIndex === -1) break; // Check if it's a complete term by ensuring what follows isn't another hyphen const endPos = foundIndex + variant.length; if (endPos >= lowerTitle.length || lowerTitle[endPos] !== '-') { foundValid = true; break; } // Move to check next occurrence startIndex = endPos; } return foundValid; } return false; } // For other non-space variations, ensure we're checking for exact matches const escapedVariant = variant.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); const regex = new RegExp(`\\b${escapedVariant}\\b`, 'i'); return regex.test(lowerTitle); }); } // Für einzelne Wörter: Einfache Wortgrenzen-Prüfung const escapedTerm = searchTerm.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); const regex = new RegExp(`\\b${escapedTerm}\\b`, 'i'); return regex.test(lowerTitle); })) 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; } // ===== Deal-Management ===== // Deal ausblenden function hideDeal(deal) { deal.style.cssText = 'display: none !important'; deal.setAttribute('data-hidden-by-mydealz-manager', 'true'); } // Letzten versteckten Deal speichern function saveLastHiddenDeal() { GM_setValue('lastHiddenDeal', lastHiddenDeal); localStorage.setItem('lastHiddenDeal', JSON.stringify(lastHiddenDeal)); } // Funktion zur Verarbeitung der Sortierung function processSortRemembering() { // Wenn Feature deaktiviert ist, lösche gespeicherte Sortierung // und lass mydealz sein Standard-Verhalten nutzen if (!window.rememberSort) { // Prüfen, ob eine gespeicherte Sortierung existiert und entfernen if (localStorage.getItem(PREFERRED_SORT_KEY)) { localStorage.removeItem(PREFERRED_SORT_KEY); if (DEBUG) console.log('[MDM Sort] Gespeicherte Sortierung gelöscht (Feature deaktiviert)'); } return; } // Prüfen, ob wir auf einer Suchseite sind if (window.location.pathname.includes('/search')) { // Parameter aus URL auslesen const params = new URLSearchParams(window.location.search); const currentSort = params.get('sortBy'); // Wenn eine Sortierung vorhanden ist, speichern if (currentSort) { localStorage.setItem(PREFERRED_SORT_KEY, currentSort); if (DEBUG) console.log('[MDM Sort] Sortierung gespeichert: ' + currentSort); } else { // Wenn keine Sortierung gesetzt ist, aber eine gespeicherte existiert const savedSort = localStorage.getItem(PREFERRED_SORT_KEY); if (savedSort && !window.location.href.includes('sortBy=')) { // Neue URL mit der gespeicherten Sortierung erstellen let newUrl = window.location.href; const separator = newUrl.includes('?') ? '&' : '?'; newUrl += separator + 'sortBy=' + savedSort; // Umleitung zur neuen URL mit kleiner Verzögerung if (DEBUG) console.log('[MDM Sort] Wende Sortierung an: ' + savedSort); setTimeout(() => { window.location.href = newUrl; }, 100); } } } // Suchformulare abfangen und anpassen const searchForms = document.querySelectorAll('form[action*="/search"]'); searchForms.forEach(form => { // Prüfen, ob das Formular bereits verarbeitet wurde if (form.dataset.sortingModified === 'true') return; // Markieren, dass das Formular verarbeitet wurde form.dataset.sortingModified = 'true'; // Event-Listener für das Absenden des Formulars hinzufügen form.addEventListener('submit', function(e) { // Nur fortfahren, wenn Feature aktiviert ist if (!window.rememberSort) return; const savedSort = localStorage.getItem(PREFERRED_SORT_KEY); if (savedSort) { if (DEBUG) console.log('[MDM Sort] Füge Sortierung zur Suchanfrage hinzu: ' + savedSort); // Prüfen, ob bereits ein sortBy-Feld vorhanden ist let sortByInput = form.querySelector('input[name="sortBy"]'); // Wenn kein sortBy-Feld existiert, eines erstellen if (!sortByInput) { sortByInput = document.createElement('input'); sortByInput.type = 'hidden'; sortByInput.name = 'sortBy'; form.appendChild(sortByInput); } // Sortierung setzen sortByInput.value = savedSort; } }); }); } //#endregion //#region --- 6. Initialisierung und Setup --- function init() { // ===== 1. Grundeinstellungen und gespeicherte Daten laden ===== // --- Grundeinstellungen --- hideCustomBanners = GM_getValue('hideCustomBanners', false); syncStorage(); excludeWords = loadExcludeWords(); loadSettings(); // --- Deal-Listen --- hiddenDeals = GM_getValue('hiddenDeals', []); recentHiddenDeals = GM_getValue('recentHiddenDeals', []); // --- Letzter versteckter Deal --- lastHiddenDeal = GM_getValue('lastHiddenDeal', null); lastHiddenDealShown = GM_getValue(LAST_HIDDEN_DEAL_SHOWN, false); // Automatische Markierung als "angezeigt" wenn kein Deal vorhanden if (!lastHiddenDeal) { lastHiddenDealShown = true; GM_setValue(LAST_HIDDEN_DEAL_SHOWN, true); } // --- Sortierung merken --- window.rememberSort = GM_getValue('rememberSort', true); processSortRemembering(); // ===== 2. Filter-Einstellungen ===== // --- Preisfilter --- maxPrice = parseFloat(localStorage.getItem(MAX_PRICE_KEY)) || 0; // --- Kalte Deals --- hideColdDeals = localStorage.getItem(HIDE_COLD_DEALS_KEY) === 'true'; // ===== 3. Feature-Flags und UI-Status ===== // --- Banner und Widgets --- window.hideCustomBanners = hideCustomBanners; updateCustomBannerIcon(); // --- Share Buttons --- window.hideShareButtons = GM_getValue('hideShareButtons', false); updateShareButtonsVisibility(window.hideShareButtons); // --- Händlernamen --- window.hideMatchingMerchantNames = GM_getValue('hideMatchingMerchantNames', false); // ===== 5. System-Initialisierung ===== // --- UI --- initializeUI(); initObserver(); } //#endregion //#region --- 7. Backup und Wiederherstellung --- // ===== Backup-Funktionen ===== // --- Datei-Erstellung --- function createBackupFile(backup, deviceName) { const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); // Neues Datumsformat const now = new Date(); const timestamp = now.toISOString() .replace('T', '_') // T durch _ ersetzen .split('.')[0] // Millisekunden entfernen .replace(/:/g, '.') // : durch . ersetzen .replace(/-/g, '-'); // - behalten a.href = url; a.download = `mydealz_backup_${deviceName}_${timestamp}.json`; a.click(); URL.revokeObjectURL(url); } // --- Backup-Prozess --- function backupData() { try { // 1. Daten laden const currentWords = loadExcludeWords(); const currentMerchants = loadExcludeMerchants(); // 2. Backup-Objekt erstellen const backup = { excludeWords: currentWords, merchantsData: currentMerchants, maxPrice: maxPrice, hideColdDeals: hideColdDeals }; // 3. Geräte-Erkennung let deviceType = "Desktop"; if (IS_TOUCH_DEVICE) { // Überprüfe, ob es ein mobiles Gerät ist const userAgent = navigator.userAgent.toLowerCase(); if (userAgent.includes('iphone') || userAgent.includes('ipad')) { deviceType = "iOS"; } else if (userAgent.includes('android')) { deviceType = "Android"; } else { deviceType = "Tablet/Touch"; } } // 4. Gerätenamen-Verwaltung const DEVICE_NAME_KEY = 'mdm_device_name'; const customDeviceName = localStorage.getItem(DEVICE_NAME_KEY) || GM_getValue(DEVICE_NAME_KEY, ''); // 5. Backup-Erstellung if (!customDeviceName) { // Neuen Gerätenamen abfragen const newName = prompt( "Wie möchtest du dieses Gerät nennen?\n" + "Dies hilft dir, Backups von verschiedenen Geräten zu unterscheiden.", deviceType ); if (newName !== null) { const deviceName = newName.trim() || deviceType; // In beiden Speichern ablegen localStorage.setItem(DEVICE_NAME_KEY, deviceName); GM_setValue(DEVICE_NAME_KEY, deviceName); // Mit dem neuen Namen das Backup erstellen createBackupFile(backup, deviceName); } } else { // Mit dem vorhandenen Namen das Backup erstellen createBackupFile(backup, customDeviceName); } } catch (error) { console.error('Backup error:', error); alert('Fehler beim Erstellen des Backups: ' + error.message); } } // ===== Wiederherstellungs-Funktionen ===== // --- Daten-Wiederherstellung --- function restoreData(event) { // 1. Datei-Validierung const file = event.target.files[0]; if (!file || file.type !== 'application/json') { console.error('Invalid file:', file); alert('Bitte wählen Sie eine gültige JSON-Datei aus.'); return; } // 2. Datei-Verarbeitung const reader = new FileReader(); reader.onload = function(e) { try { // 3. Daten-Parsing const restoredData = JSON.parse(e.target.result); // 4. Aktuelle Daten laden const currentWords = new Set(loadExcludeWords()); const currentMerchants = new Map( loadExcludeMerchants().map(m => [m.id, m]) ); // 5. Daten zusammenführen // --- Wörter --- restoredData.excludeWords.forEach(word => currentWords.add(word)); const mergedWords = Array.from(currentWords); // --- Händler --- restoredData.merchantsData.forEach(merchant => { if (!currentMerchants.has(merchant.id)) { currentMerchants.set(merchant.id, merchant); } }); const mergedMerchants = Array.from(currentMerchants.values()); // 6. Daten speichern // --- Wortfilter --- GM_setValue('excludeWords', mergedWords); localStorage.setItem('excludeWords', JSON.stringify(mergedWords)); excludeWords = mergedWords; // --- Händlerfilter --- saveExcludeMerchants(mergedMerchants); // --- Einstellungen --- 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); } // 7. UI aktualisieren if (isSettingsOpen) { updateUITheme(); } processArticles(); alert('Backup wurde erfolgreich wiederhergestellt.'); } catch (error) { console.error('Restore error:', error); alert('Fehler beim Wiederherstellen des Backups: ' + error.message); } }; reader.readAsText(file); } //#endregion //#region --- 8. Hilfsfunktionen --- // ===== Text-Verarbeitung ===== // --- Händlernamen-Verarbeitung --- function removeMerchantNameFromTitle(title, merchant) { if (!title || !merchant) return title; // Normalisiere Händlernamen und hole Konfiguration const merchantName = merchant.toLowerCase() .replace('...', '') // Extrahiere den Basis-Namen aus dem Händlernamen, wenn es sich um eine Domain handelt let baseShopName = merchantName; if (merchantName.includes('-shop')) { baseShopName = merchantName.split('-shop')[0]; } // Händler-spezifische Transformationen definieren const getMerchantConfig = (merchant) => { switch (merchant) { case 'Netto Marken-Discount': return { abbreviations: ['netto md'], replacements: [ // Entfernt "Netto Marken Discount -" nach lokaler Angabe { from: /(\[\s*lokal[^\]]+\])\s*netto\s+marken[-\s]discount\s*-\s*/i, to: '$1 ' }, // Entfernt "bei Netto Marken-Discount gibt es" { from: /\s+bei\s+netto\s+marken[-\s]discount\s+gibt\s+es\s*/i, to: ' ' }, // Standardfälle für Netto { from: /(? { result = result.replace(from, to); }); // Spezielle Muster für [Händler| Format] result = result.replace(new RegExp(`\\[${merchantName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}\\|\\s*`, 'i'), '['); // Wenn wir einen Basis-Namen extrahiert haben, auch diesen behandeln if (baseShopName !== merchantName) { result = result.replace(new RegExp(`\\[${baseShopName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}\\|\\s*`, 'i'), '['); } const domainVariants = [ // Bestehende Varianten `${merchantName}.com`, `${merchantName}.de`, `${merchantName}.co.uk`, // Neue Varianten mit 'shop' `${merchantName}-shop.com`, `${merchantName}-shop.de` ]; // Erstelle Varianten des Händlernamens const merchantVariants = [ merchantName, merchantName.replace(/\s+/g, '-'), merchantName.replace(/\-/g, ' '), ...config.abbreviations, ...domainVariants ]; // Den Basisnamen auch zu den Varianten hinzufügen if (baseShopName !== merchantName) { merchantVariants.push(baseShopName); merchantVariants.push(baseShopName.replace(/\s+/g, '-')); merchantVariants.push(baseShopName.replace(/\-/g, ' ')); } // Und, in umgekehrter Richtung, wenn der Händlername bereits eine Domain ist if (merchantName.includes('.') || merchantName.includes('-shop')) { // Entferne TLD und mögliche Zusätze wie "-shop" const baseName = merchantName .split('.')[0] // Entferne TLD (.com, .de, etc.) .replace(/-shop$/, '') // Entferne mögliches "-shop" am Ende .replace(/[^\w\s-]/g, ''); // Entferne alle Sonderzeichen außer Bindestriche und Leerzeichen merchantVariants.push(baseName); // Füge auch Varianten mit unterschiedlicher Groß-/Kleinschreibung hinzu merchantVariants.push(baseName.toLowerCase()); merchantVariants.push(baseName.toUpperCase()); } // Entferne Händlernamen merchantVariants.forEach(variant => { const escapedVariant = variant.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); // KORRIGIERT: Unterscheide zwischen normalen Klammern und Klammern mit zu behaltenden Marken if (config.keepBrands.length > 0) { // Für Fälle wie [Kaufland Card] -> [K-Card], aber [Kaufland] -> "" const keepBrandsPattern = config.keepBrands.join('|'); result = result.replace( new RegExp(`\\[${escapedVariant}\\](?!\\s*(?:${keepBrandsPattern}))`, 'i'), '' ); // Zusätzlich: Entferne auch am Ende des Titels result = result.replace( new RegExp(`\\[${escapedVariant}\\]\\s*$`, 'i'), '' ); } else { // Für Händler ohne spezielle Marken, entferne einfach die Klammern result = result.replace(new RegExp(`\\[${escapedVariant}\\]`, 'i'), ''); } result = result // Die restlichen Standardmuster .replace(new RegExp(`\\(${escapedVariant}:\\s*`, 'i'), '(') .replace(new RegExp(`${escapedVariant}:\\s*`, 'i'), '') // Händler am Anfang (mit optionalem Punkt danach) .replace(new RegExp(`^${escapedVariant}\\s*\\.?\\s*[-–]?\\s*`, 'i'), '') // Händler mit Punkt als Trenner (am Anfang, in der Mitte oder am Ende) .replace(new RegExp(`^${escapedVariant}\\.\\s*`, 'i'), '') .replace(new RegExp(`\\s+${escapedVariant}\\.\\s+`, 'i'), ' ') .replace(new RegExp(`\\s+${escapedVariant}\\.\\s*$`, 'i'), '') // Händler am Ende oder in der Mitte mit "bei" .replace(new RegExp(`\\s+bei\\s+${escapedVariant}\\s+`, 'i'), ' ') .replace(new RegExp(`\\s+(?:bei\\s+)?${escapedVariant}(?:[-–]|\\s)*$`, 'i'), '') // Händler in Klammern mit fehlenden Klammern (spezifischer) .replace(new RegExp(`^${escapedVariant}\\)\\s*`, 'i'), '') // Fehlende öffnende Klammer .replace(new RegExp(`\\(${escapedVariant}$`, 'i'), '') // Fehlende schließende Klammer // Händler in Klammern (mit optionalen Klammern) .replace(new RegExp(`\\(?${escapedVariant}\\)?\\s*`, 'i'), '') .replace(new RegExp(`\\(${escapedVariant}\\s+`, 'i'), '('); }); return result.replace(/\s+/g, ' ').trim(); } function shortenMerchantName(title) { // Special cases for merchant names const replacements = { 'Kaufland Card': 'K-Card', 'Kaufland': '', // Add more special cases here if needed }; let newTitle = title; for (const [merchant, replacement] of Object.entries(replacements)) { // Case insensitive replace with word boundaries const regex = new RegExp(`\\b${merchant}\\b`, 'i'); newTitle = newTitle.replace(regex, replacement); } return newTitle.trim(); } // ===== UI-Management ===== // --- Cleanup & Reset --- function cleanup() { // Remove settings UI and always reset state if (settingsDiv?.parentNode) settingsDiv.remove(); isSettingsOpen = false; // Der wichtige Teil - wenn das UI geschlossen wird und ein Deal ausgeblendet wurde if (lastHiddenDeal && !lastHiddenDealShown) { lastHiddenDealShown = true; GM_setValue(LAST_HIDDEN_DEAL_SHOWN, true); } // Add word suggestion list cleanup const suggestionList = document.getElementById('wordSuggestionList'); if (suggestionList) { suggestionList.remove(); } // Close merchant & words lists with state logging if (merchantListDiv?.parentNode) { merchantListDiv.remove(); } if (wordsListDiv?.parentNode) { wordsListDiv.remove(); } // Schließe auch das Sidebar Elements UI wenn vorhanden const sidebarElementsDiv = document.getElementById('sidebarElementsDiv'); if (sidebarElementsDiv?.parentNode) { sidebarElementsDiv.remove(); } // Reset UI states with logging if (activeSubUI) { 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; dealThatOpenedSettings = null; // Reset auch den aktiven Deal // Clean up handlers document.removeEventListener('click', suggestionClickHandler); document.removeEventListener('click', uiClickOutsideHandler); window.removeEventListener('unload', cleanup); uiClickOutsideHandler = null; // Reset suggestion state suggestedWords = []; } //#endregion //#region --- 9. Theming und UI-Darstellung --- // Farbkonstanten für Light/Dark Mode const THEME_COLORS = { light: { background: '#ffffff', border: 'rgba(3,12,25,0.23)', text: '#333333', buttonBg: '#f5f5f5', buttonBorder: '#d0d0d0', inputBg: '#ffffff', itemBg: '#f8f8f8' }, dark: { background: '#1d1f20', border: 'rgb(107, 109, 109)', text: '#ffffff', buttonBg: '#2d2d2d', buttonBorder: '#3d3d3d', inputBg: '#1d1f20', itemBg: '#2a2a2a' } }; // Theme Observer erstellen const themeObserver = new MutationObserver(() => { requestAnimationFrame(() => { const colors = getThemeColors(); updateAllUIThemes(colors); }); }); // 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 colors = getThemeColors(); updateAllUIThemes(colors); }); }); // Hide Button Theme Observer const hideButtonThemeObserver = new MutationObserver(() => { const colors = getThemeColors(); requestAnimationFrame(() => { document.querySelectorAll('.custom-hide-button').forEach(button => { if (button) { button.style.cssText = ` position: absolute !important; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; z-index: 10002 !important; background: ${colors.background} !important; border: 1px solid ${colors.border} !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() { const colors = getThemeColors(); // Update buttons document.querySelectorAll('.custom-hide-button').forEach(button => { if (button) { button.style.setProperty('background', colors.background, 'important'); button.style.setProperty('border-color', colors.border, 'important'); } }); // Update open UIs if (isSettingsOpen || activeSubUI) { updateUITheme(); } } //#endregion //#region --- 10. Button-Management --- 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: ${getThemeColors().background} !important; border: 1px solid ${getThemeColors().border} !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 { const dealId = deal.getAttribute('id'); const dealTitle = deal.querySelector('.thread-title')?.textContent.trim() || 'Unbekannter Deal'; // Aktuellen Deal speichern lastHiddenDeal = { id: dealId, title: dealTitle, timestamp: Date.now() }; // "Deal wurde angezeigt" auf false setzen lastHiddenDealShown = false; GM_setValue(LAST_HIDDEN_DEAL_SHOWN, false); // Speichern des letzten ausgeblendeten Deals saveLastHiddenDeal(); 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'); const dealTitle = deal.querySelector('.thread-title')?.textContent.trim() || 'Unbekannter Deal'; // Aktuellen Deal speichern lastHiddenDeal = { id: dealId, title: dealTitle, timestamp: Date.now() }; // "Deal wurde angezeigt" auf false setzen und persistieren lastHiddenDealShown = false; GM_setValue(LAST_HIDDEN_DEAL_SHOWN, false); saveLastHiddenDeal(); hiddenDeals.push(dealId); saveHiddenDeals(); hideDeal(deal); return false; }; } hideButtonContainer.appendChild(hideButton); targetElement.appendChild(hideButtonContainer); deal.setAttribute('data-button-added', 'true'); }); } function getMerchantButtonText(merchantName) { return `Alle Deals von ${merchantName}`; } function addMerchantPageHideButton() { 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; const hideButtonContainer = document.createElement('div'); hideButtonContainer.style.cssText = ` display: inline-flex; align-items: center; margin-left: 10px; `; const hideButton = document.createElement('button'); hideButton.style.cssText = ` display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; font-size: 14px; `; const buttonText = getMerchantButtonText(merchantName); hideButton.innerHTML = ` ${buttonText} `; hideButton.title = `${buttonText} ausblenden`; hideButton.addEventListener('click', () => { const merchantsData = loadExcludeMerchants(); if (!merchantsData.some(m => m.id === merchantId)) { merchantsData.unshift({ id: merchantId, name: merchantName }); saveExcludeMerchants(merchantsData); processArticles(); } }); hideButtonContainer.appendChild(hideButton); merchantBanner.appendChild(hideButtonContainer); } // Helfer-Funktion, um den Zustand der Toggle-Buttons zu aktualisieren function updateToggleButtonsState() { const buttons = document.querySelectorAll('.toggle-sidebar-element'); buttons.forEach(button => { const key = button.dataset.key; if (key && SIDEBAR_ELEMENTS[key]) { const element = SIDEBAR_ELEMENTS[key]; const icon = button.querySelector('i'); if (icon) { icon.className = `fas ${element.hidden ? 'fa-eye-slash' : 'fa-eye'}`; icon.setAttribute('aria-label', element.hidden ? 'Element versteckt' : 'Element sichtbar'); } } }); } //#endregion //#region --- 11. Skript-Initialisierung --- // Initial beim Start aufrufen registerMenuCommands(); // 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); })(); //#endregion //#region --- 12. Spezialisierte Komponenten --- function initializeUI() { // Initial UI Setup processArticles(); addSettingsButton(); addHideButtons(); addMerchantPageHideButton(); } // Observer Initialisierung function initObserver() { observer.disconnect(); observer.observe(document.body, { childList: true, subtree: true }); // Sofortige Verarbeitung requestAnimationFrame(() => { processArticles(); addSettingsButton(); addHideButtons(); }); } function registerMenuCommands() { // Alte Menüs erst abmelden if (menuCommandId !== undefined) { GM_unregisterMenuCommand(menuCommandId); } // Menüeintrag zum Öffnen der Einstellungen registrieren menuCommandId = GM_registerMenuCommand('mydealz Manager Einstellungen', () => { // Einstellungen für aktuellen Deal öffnen dealThatOpenedSettings = document.querySelector('article.thread--deal, article.thread--voucher'); createSettingsUI(); }); } //#endregion //#region --- 13. Dokumentation --- /* =================================================================================== --- Funktionsübersicht mydealz Manager --- =================================================================================== detectMultipleInstances() - Erkennt parallele Ausführungen des Scripts (ab 1.13.x) getThemeColors() - Liefert Theme-spezifische Farben basierend auf aktuellem Theme processArticles() - Verarbeitet und filtert alle Deals shouldExcludeArticle() - Prüft ob ein Deal ausgeblendet werden soll createSettingsUI() - Erstellt das Haupteinstellungsfenster addSettingsButton() - Fügt Einstellungs-Button zu Deals hinzu addHideButtons() - Fügt X-Button zum Ausblenden hinzu backupData() - Erstellt Backup der Einstellungen restoreData() - Stellt Backup-Daten wieder her decodeHtml() - Konvertiert HTML-Entities cleanup() - Räumt UI-Elemente auf syncStorage() - Synchronisiert GM und localStorage saveExcludeWords() - Speichert Wortfilter loadExcludeWords() - Lädt Wortfilter saveExcludeMerchants() - Speichert Händlerfilter loadExcludeMerchants() - Lädt Händlerfilter saveMaxPrice() - Speichert Maximalpreis createMerchantListUI() - Zeigt Händlerliste createExcludeWordsUI() - Zeigt Wortfilterliste updateActiveLists() - Aktualisiert Listen im UI handleMerchantDelete() - Löscht Händler aus Filter handleWordDelete() - Löscht Wort aus Filter setupScrollHandling() - Konfiguriert Scroll-Verhalten updateUITheme() - Aktualisiert UI-Farben init() - Initialisiert das Script removeMerchantNameFromTitle() - Entfernt Händlernamen aus Deal-Titeln throttle() - Begrenzt die Ausführungshäufigkeit von Funktionen getWordsFromTitle() - Extrahiert relevante Wörter aus Deal-Titeln hideDeal() - Blendet einen Deal aus initUIContainers() - Initialisiert UI-Container updateSuggestionList() - Aktualisiert Wortvorschläge handleWordSelection() - Verarbeitet Wortauswahl setupClickOutsideHandler() - Konfiguriert Außenbereich-Klicks createSuggestionClickHandler() - Erstellt Handler für Wortvorschläge registerMenuCommands() - Registriert Script-Manager Menüeinträge saveHiddenDeals() - Speichert ausgeblendete Deals saveLastHiddenDeal() - Speichert zuletzt ausgeblendeten Deal initializeUI() - Initialisiert Benutzeroberfläche initObserver() - Initialisiert DOM-Beobachter addMerchantPageHideButton() - Fügt Händler-Ausblenden-Button hinzu injectDealFilters() - Fügt Deal-Filter in die UI ein =================================================================================== */ //#endregion