// ==UserScript== // @name Der mydealz Manager: Deine persönliche Deal-Zentrale 🚀 // @namespace http://tampermonkey.net/ // @version 1.18 // @description Mächtiges Filter- & UI-Toolkit: Blockiert Deals nach Wort/Händler/Preis/Temp, entfernt Werbung, speichert Einstellungen & mehr. // @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 https://update.greasyfork.icu/scripts/522038/Der%20mydealz%20Manager%3A%20Deine%20pers%C3%B6nliche%20Deal-Zentrale%20%F0%9F%9A%80.user.js // @updateURL https://update.greasyfork.icu/scripts/522038/Der%20mydealz%20Manager%3A%20Deine%20pers%C3%B6nliche%20Deal-Zentrale%20%F0%9F%9A%80.meta.js // ==/UserScript== // Versions-Änderungen // FIX: SettingsUI wurde in seltenen Fällen nicht geöffnet // CHANGE: Funktion um Händlernamen im Titel auszublenden wurde verbessert //#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 PREFERRED_SORT_KEY = 'mydealz_preferred_sort'; const PREFERRED_TIMEFRAME_KEY = 'mydealz_preferred_timeframe'; // --- 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); // --- 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 blockedUsersDiv = 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; // ===== Menü-Commands ===== // --- Command IDs --- let menuCommandId; 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 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'); blockedUsersDiv = 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() { // UI erstellen const settingsUI = document.createElement('div'); if (isSettingsOpen) { return; } isSettingsOpen = true; // Lade versteckte Deals neu hiddenDeals = GM_getValue('hiddenDeals', []); // Initialize containers initUIContainers(); const colors = getThemeColors(); // Get merchant info from current deal let merchantName = null; let showMerchantButton = false; let userButtonHTML = ''; settingsDiv.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 300px; height: 300px; max-width: 90vw; max-height: 90vh; background: ${colors.background}; border: 1px solid ${colors.border}; border-radius: 5px; padding: 8px 15px; z-index: 1000; color: ${colors.text}; display: flex; flex-direction: column; `; if (dealThatOpenedSettings) { // Merchant Info const merchantLink = dealThatOpenedSettings.querySelector('a[data-t="merchantLink"]'); if (merchantLink) { merchantName = merchantLink.textContent.trim(); showMerchantButton = true; } // User Info const userSpan = dealThatOpenedSettings.querySelector('.overflow--ellipsis.size--all-xs.size--fromW3-s'); const userName = userSpan?.textContent.match(/Veröffentlicht von\s+(\S+)/)?.[1]; if (userName) { userButtonHTML = ` `; } } // Process articles when opening settings processArticles(); // Header const header = document.createElement('div'); header.className = 'accordion-header'; // Klasse für Drag-Funktionalität header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; flex-shrink: 0; cursor: move; // Cursor für Drag-Anzeige padding: 10px; user-select: none; background: ${colors.background}; border-bottom: 1px solid ${colors.border}; `; // 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; max-height: calc(300px - 60px); `; 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 ? `
` : ''} ${dealThatOpenedSettings ? (() => { const userSpan = dealThatOpenedSettings.querySelector('.color--text-TranslucentSecondary.overflow--wrap-off span.overflow--ellipsis'); const userName = userSpan?.textContent.match(/Veröffentlicht von\s+(\S+)/)?.[1]; return userName ? `
` : ''; })() : ''}
`; // 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
Sucheinstellungen speichern
`; // 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; 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); // Sammle alle Cleanup-Funktionen const cleanupFunctions = []; // Füge Draggable Cleanup hinzu const cleanupDraggable = makeDraggable(settingsDiv); if (cleanupDraggable) cleanupFunctions.push(cleanupDraggable); // Füge Scroll Handling Cleanup hinzu const cleanupScrollHandling = setupScrollHandling(); if (cleanupScrollHandling) cleanupFunctions.push(cleanupScrollHandling); // Erweitere die Hauptcleanup-Funktion einmalig const oldCleanup = cleanup; cleanup = () => { // Führe alle registrierten Cleanup-Funktionen aus cleanupFunctions.forEach(fn => fn()); // Führe original Cleanup aus oldCleanup(); }; // 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); } // 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; } .accordion-header { font-size: 18px; font-weight: bold; padding: 6px 8px !important; color: var(--primary-color, #24a300) !important; } .accordion-content { padding: 6px 8px !important; } `; document.head.appendChild(accordionSectionStyle); // 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(); } } }); } // Event Listener für den Button zum Ausblenden von User-Deals const hideUserDealsButton = document.getElementById('hideUserDealsButton'); if (hideUserDealsButton) { hideUserDealsButton.addEventListener('click', () => { if (!dealThatOpenedSettings) return; const userSpan = dealThatOpenedSettings.querySelector('.color--text-TranslucentSecondary.overflow--wrap-off span.overflow--ellipsis'); const userName = userSpan?.textContent.match(/Veröffentlicht von\s+(\S+)/)?.[1]; if (userName) { const blockedUsers = GM_getValue('blockedUsers', []); if (!blockedUsers.includes(userName)) { blockedUsers.push(userName); GM_setValue('blockedUsers', blockedUsers); processArticles(); cleanup(); } } }); } // Show Words List Button const showWordsListButton = document.getElementById('showWordsListButton'); if (showWordsListButton) { showWordsListButton.addEventListener('click', () => { if (switchSubUI('words')) { createExcludeWordsUI(); } }); } // Show Merchant List Button const showMerchantListButton = document.getElementById('showMerchantListButton'); if (showMerchantListButton) { showMerchantListButton.addEventListener('click', () => { if (switchSubUI('merchant')) { createMerchantListUI(); } }); } // Button für Blockierte Benutzer: const showBlockedUsersButton = document.getElementById('showBlockedUsersButton'); if (showBlockedUsersButton) { showBlockedUsersButton.addEventListener('click', () => { if (switchSubUI('users')) { createBlockedUsersUI(); } }); } // Sidebar Elements Button document.getElementById('openSidebarElementsButton').addEventListener('click', () => { if (switchSubUI('sidebar')) { createSidebarElementsUI(); } }); // 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) : []; // 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(); // 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('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 makeDraggable(element) { // Don't make draggable on touch devices if (IS_TOUCH_DEVICE) return; let isDragging = false; let offsetX = 0, offsetY = 0; let wasMoved = false; // Header für Drag-Funktionalität const header = element.querySelector('.accordion-header:first-child'); if (!header) return; header.style.cursor = 'move'; header.style.userSelect = 'none'; const startDrag = (e) => { // Nur bei linker Maustaste if (e.button !== 0) return; isDragging = true; const rect = element.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; // Initial position setzen wenn noch nicht verschoben if (!wasMoved) { element.style.left = rect.left + 'px'; element.style.top = rect.top + 'px'; element.style.transform = 'none'; element.style.right = 'auto'; element.style.bottom = 'auto'; wasMoved = true; } // Prevent text selection during drag document.body.style.userSelect = 'none'; }; const drag = (e) => { if (!isDragging) return; const newLeft = e.clientX - offsetX; const newTop = e.clientY - offsetY; const rect = element.getBoundingClientRect(); const maxX = window.innerWidth - rect.width; const maxY = window.innerHeight - rect.height; // Hauptfenster Position aktualisieren const finalLeft = Math.min(Math.max(0, newLeft), maxX); const finalTop = Math.min(Math.max(0, newTop), maxY); element.style.left = finalLeft + 'px'; element.style.top = finalTop + 'px'; // Direkt die Sub-UIs aktualisieren const settingsRect = element.getBoundingClientRect(); [ wordsListDiv, merchantListDiv, blockedUsersDiv, document.getElementById('sidebarElementsDiv'), document.getElementById('wordSuggestionList') ].forEach(ui => { if (ui?.parentNode) { ui.style.position = 'fixed'; ui.style.top = `${settingsRect.top}px`; ui.style.left = `${settingsRect.right + 10}px`; } }); }; const stopDrag = () => { if (!isDragging) return; isDragging = false; document.body.style.userSelect = ''; // Position speichern const rect = element.getBoundingClientRect(); GM_setValue('mdmSettingsPos', JSON.stringify({ left: rect.left, top: rect.top, wasMoved: true })); // Finale Position der Sub-UIs aktualisieren const settingsRect = element.getBoundingClientRect(); [ wordsListDiv, merchantListDiv, blockedUsersDiv, document.getElementById('sidebarElementsDiv'), document.getElementById('wordSuggestionList') ].forEach(ui => { if (ui?.parentNode) { ui.style.position = 'fixed'; ui.style.top = `${settingsRect.top}px`; ui.style.left = `${settingsRect.right + 10}px`; } }); }; header.addEventListener('mousedown', startDrag); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag); // Gespeicherte Position wiederherstellen const savedPos = GM_getValue('mdmSettingsPos'); if (savedPos) { try { const pos = JSON.parse(savedPos); if (pos.wasMoved) { wasMoved = true; element.style.left = pos.left + 'px'; element.style.top = pos.top + 'px'; element.style.transform = 'none'; element.style.right = 'auto'; element.style.bottom = 'auto'; // Auch die Sub-UIs an die gespeicherte Position anpassen const settingsRect = element.getBoundingClientRect(); [ wordsListDiv, merchantListDiv, blockedUsersDiv, document.getElementById('sidebarElementsDiv'), document.getElementById('wordSuggestionList') ].forEach(ui => { if (ui?.parentNode) { ui.style.position = 'fixed'; ui.style.top = `${settingsRect.top}px`; ui.style.left = `${settingsRect.right + 10}px`; } }); } } catch (e) { console.error('Error restoring settings position:', e); } } // Cleanup-Funktion zurückgeben return () => { header.removeEventListener('mousedown', startDrag); document.removeEventListener('mousemove', drag); document.removeEventListener('mouseup', stopDrag); }; } 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(); } }); } // Togglen der Feed-Banner 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(); } // Blockierte Benutzer UI erstellen function createBlockedUsersUI() { const colors = getThemeColors(); blockedUsersDiv.id = 'blockedUsersDiv'; blockedUsersDiv.style.cssText = ` ${getSubUIPosition()} padding: 15px; background: ${colors.background}; border: 1px solid ${colors.border}; border-radius: 5px; width: 300px; color: ${colors.text}; `; // Lade blockierte Benutzer const blockedUsers = GM_getValue('blockedUsers', []); const blockedUsersHTML = blockedUsers.map(user => `
${user}
`).join(''); blockedUsersDiv.innerHTML = `

Ausgeblendete Benutzer (${blockedUsers.length})

${blockedUsersHTML}
`; document.body.appendChild(blockedUsersDiv); setupClickOutsideHandler(); // Nach dem Hinzufügen zum DOM den Button anpassen document.getElementById('closeBlockedUsersButton').style.cssText = ` padding: 8px 16px; display: inline-block; width: auto; min-width: 100px; text-align: center; `; // Suchfunktionalität const searchInput = document.getElementById('userSearch'); searchInput.addEventListener('input', (e) => { const searchTerm = e.target.value.toLowerCase(); let visibleCount = 0; const totalCount = blockedUsers.length; document.querySelectorAll('.user-item').forEach(item => { const userName = item.querySelector('span').textContent.toLowerCase(); const isVisible = userName.includes(searchTerm); item.style.display = isVisible ? 'flex' : 'none'; if (isVisible) visibleCount++; }); // Update heading counter const heading = blockedUsersDiv.querySelector('h4'); if (heading) { heading.textContent = searchTerm ? `Ausgeblendete Benutzer (${visibleCount}/${totalCount})` : `Ausgeblendete Benutzer (${totalCount})`; } }); // Alle Benutzer entfernen Button document.getElementById('clearBlockedUsersButton').addEventListener('click', () => { if (confirm('Möchten Sie wirklich alle Benutzer aus der Liste entfernen?')) { GM_setValue('blockedUsers', []); document.getElementById('usersList').innerHTML = ''; processArticles(); // Immediately update counter in heading const heading = blockedUsersDiv.querySelector('h4'); if (heading) { heading.textContent = 'Ausgeblendete Benutzer (0)'; } } }); // Einzelne Benutzer entfernen document.querySelectorAll('.delete-user').forEach(button => { button.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const userToDelete = button.dataset.user; const userItem = button.closest('.user-item'); const blockedUsers = GM_getValue('blockedUsers', []); // Update blockedUsers array const updatedUsers = blockedUsers.filter(user => user !== userToDelete); GM_setValue('blockedUsers', updatedUsers); // Update UI userItem.remove(); processArticles(); // Update counter in heading const heading = blockedUsersDiv.querySelector('h4'); if (heading) { heading.textContent = `Ausgeblendete Benutzer (${updatedUsers.length})`; } }); }); // Schließen Button document.getElementById('closeBlockedUsersButton').addEventListener('click', (e) => { e.stopPropagation(); closeActiveSubUI(); }); } // === 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; `; // 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); }; } // 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'}`; icon.setAttribute('aria-label', element.hidden ? 'Element versteckt' : 'Element sichtbar'); } }); 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 blockedUsersOpen = blockedUsersDiv?.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)) && (!blockedUsersOpen || !blockedUsersDiv.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(); if (blockedUsersDiv?.parentNode) blockedUsersDiv.remove(); if (document.getElementById('sidebarElementsDiv')?.parentNode) { document.getElementById('sidebarElementsDiv').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 settingsRect = settingsDiv?.getBoundingClientRect(); if (!settingsRect) return ''; if (IS_TOUCH_DEVICE) { return ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10002; `; } else { const gap = 10; return ` position: fixed; top: ${settingsRect.top}px; left: ${settingsRect.right + gap}px; z-index: 10002; `; } } // Funktion zum Aktualisieren der Sub-UI Positionen function updateSubUIPositions() { // Skip position updates on mobile if (IS_TOUCH_DEVICE) return; // Settings-UI Position ermitteln const settingsDiv = document.getElementById('mdm-settings-popup') || document.querySelector('[id^="mdm-settings"]'); if (!settingsDiv) return; const settingsRect = settingsDiv.getBoundingClientRect(); const subUIs = [ document.getElementById('wordsListDiv'), document.getElementById('merchantListDiv'), document.getElementById('sidebarElementsDiv') ]; // Aktualisiere Position für jedes vorhandene Sub-UI subUIs.forEach(ui => { if (ui?.parentNode) { ui.style.position = 'fixed'; ui.style.top = `${settingsRect.top}px`; ui.style.left = `${settingsRect.right + 10}px`; // 10px Abstand ui.style.zIndex = '10002'; } }); } // 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); } // Helper function to manage Sub-UI states function switchSubUI(newUI) { // If trying to open the same UI that's already active, close it if (activeSubUI === newUI) { closeActiveSubUI(); return false; } // Close any active Sub-UI first if (activeSubUI) { closeActiveSubUI(); } // Set new active UI activeSubUI = newUI; // Update button texts const merchantButton = document.getElementById('showMerchantListButton'); const wordsButton = document.getElementById('showWordsListButton'); if (merchantButton) { merchantButton.innerHTML = activeSubUI === 'merchant' ? ' Händlerfilter schließen' : ' Händlerfilter verwalten'; } if (wordsButton) { wordsButton.innerHTML = activeSubUI === 'words' ? ' Wortfilter schließen' : ' Wortfilter verwalten'; } return true; } function closeActiveSubUI() { if (activeSubUI === 'merchant') { merchantListDiv?.remove(); } else if (activeSubUI === 'words') { wordsListDiv?.remove(); } else if (activeSubUI === 'sidebar') { document.getElementById('sidebarElementsDiv')?.remove(); } else if (activeSubUI === 'users') { blockedUsersDiv?.remove(); } // Reset button texts const merchantButton = document.getElementById('showMerchantListButton'); const wordsButton = document.getElementById('showWordsListButton'); if (merchantButton) { merchantButton.innerHTML = ' Händlerfilter verwalten'; merchantButton.removeAttribute('data-processing'); } if (wordsButton) { wordsButton.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'; hiddenCount++; } else { // Sicherstellen, dass der Artikel sichtbar ist article.style.display = ''; } }); 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'); // Suche zuerst nach merchant-id Link const merchantLink = deal.querySelector('a[data-t="merchantLink"]'); // Als Fallback den "Verfügbar bei" Text const merchantSpan = deal.querySelector('.color--text-TranslucentSecondary.size--all-xs span:last-child'); if (titleElement) { let merchantName = null; // Händlername entweder aus Link oder Span ermitteln if (merchantLink) { merchantName = merchantLink.textContent.trim(); } else if (merchantSpan) { merchantName = merchantSpan.textContent.trim(); } if (merchantName) { 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'); if (titleLink && ORIGINAL_TITLES.has(dealId)) { titleLink.textContent = ORIGINAL_TITLES.get(dealId); } } }); // Sucheinstellungen verarbeiten processSucheinstellungen(); } // ===== 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; } } // 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('-')) { // Keine Variationen mehr erzeugen - nur exakte Matches erlauben const variations = [searchTerm]; 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; } // ZUSÄTZLICHE PRÜFUNG: Wenn der Suchbegriff eine exakte Teilmenge des Titels ist const spaceJoinedSearchTerm = words.join(' '); if (lowerTitle.includes(spaceJoinedSearchTerm)) { return true; } // Wenn keine exakte Übereinstimmung gefunden wurde, ist es kein Match return false; } // For hyphenated variations, ensure exact match with proper boundaries if (variant.includes('-')) { // Exact hyphenated match required const escapedVariant = variant.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); const regex = new RegExp(`\\b${escapedVariant}\\b`, 'i'); return regex.test(lowerTitle); } return false; }); } // 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; // Prüfe auf blockierte User const userSpan = article.querySelector('.color--text-TranslucentSecondary.overflow--wrap-off span.overflow--ellipsis'); const userName = userSpan?.textContent.match(/Veröffentlicht von\s+(\S+)/)?.[1]; const blockedUsers = GM_getValue('blockedUsers', []); if (userName && blockedUsers.includes(userName)) { 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'); } // Funktion zum Speichern der Sucheinstellungen function processSucheinstellungen() { // Wenn Feature deaktiviert ist, lösche gespeicherte Einstellungen if (!window.rememberSort) { // Prüfen und entfernen der gespeicherten Einstellungen if (localStorage.getItem(PREFERRED_SORT_KEY)) { localStorage.removeItem(PREFERRED_SORT_KEY); } if (localStorage.getItem(PREFERRED_TIMEFRAME_KEY)) { localStorage.removeItem(PREFERRED_TIMEFRAME_KEY); } 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'); const currentTimeframe = params.get('time_frame'); // Wenn Sortierung vorhanden ist, speichern if (currentSort) { localStorage.setItem(PREFERRED_SORT_KEY, currentSort); } // Wenn Zeitraum vorhanden ist, speichern if (currentTimeframe) { localStorage.setItem(PREFERRED_TIMEFRAME_KEY, currentTimeframe); } // Wenn keine Sortierung/Zeitraum gesetzt ist, aber gespeicherte Einstellungen existieren const savedSort = localStorage.getItem(PREFERRED_SORT_KEY); const savedTimeframe = localStorage.getItem(PREFERRED_TIMEFRAME_KEY); if ((savedSort || savedTimeframe) && (!params.has('sortBy') || !params.has('time_frame'))) { // Neue URL mit den gespeicherten Einstellungen erstellen let newUrl = window.location.href; const separator = newUrl.includes('?') ? '&' : '?'; const additionalParams = []; if (savedSort && !params.has('sortBy')) { additionalParams.push('sortBy=' + savedSort); } if (savedTimeframe && !params.has('time_frame')) { additionalParams.push('time_frame=' + savedTimeframe); } if (additionalParams.length > 0) { newUrl += separator + additionalParams.join('&'); window.location.href = newUrl; } } } // 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); const savedTimeframe = localStorage.getItem(PREFERRED_TIMEFRAME_KEY); if (savedSort || savedTimeframe) { // Sortierung hinzufügen if (savedSort) { let sortByInput = form.querySelector('input[name="sortBy"]'); if (!sortByInput) { sortByInput = document.createElement('input'); sortByInput.type = 'hidden'; sortByInput.name = 'sortBy'; form.appendChild(sortByInput); } sortByInput.value = savedSort; } // Zeitraum hinzufügen if (savedTimeframe) { let timeframeInput = form.querySelector('input[name="time_frame"]'); if (!timeframeInput) { timeframeInput = document.createElement('input'); timeframeInput.type = 'hidden'; timeframeInput.name = 'time_frame'; form.appendChild(timeframeInput); } timeframeInput.value = savedTimeframe; } } }); }); } // Funktion zum Ausblenden von User-Deals function handleUserBlock(article) { // Versuche den Benutzernamen zu finden const userSpan = article.querySelector('.overflow--ellipsis.size--all-xs.size--fromW3-s'); const userName = userSpan?.textContent.match(/Veröffentlicht von\s+(\S+)/)?.[1]; if (!userName) return; // Bestätigungsdialog if (confirm(`Möchten Sie wirklich alle Deals von ${userName} ausblenden?`)) { // Speichere den Benutzer in der Liste const blockedUsers = GM_getValue('blockedUsers', []); if (!blockedUsers.includes(userName)) { blockedUsers.push(userName); GM_setValue('blockedUsers', blockedUsers); // Aktualisiere die Anzeige processArticles(); } } } //#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', []); // --- Sortierung merken --- window.rememberSort = GM_getValue('rememberSort', true); processSucheinstellungen(); // ===== 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]) ); const currentBlockedUsers = new Set(GM_getValue('blockedUsers', [])); // 5. Zähler für neue Einträge initialisieren const changes = { words: 0, merchants: 0, users: 0 }; // 6. Daten zusammenführen // --- Wörter --- restoredData.excludeWords.forEach(word => { if (!currentWords.has(word)) { currentWords.add(word); changes.words++; } }); const mergedWords = Array.from(currentWords); // --- Händler --- restoredData.merchantsData.forEach(merchant => { if (!currentMerchants.has(merchant.id)) { currentMerchants.set(merchant.id, merchant); changes.merchants++; } }); const mergedMerchants = Array.from(currentMerchants.values()); // --- Blockierte Benutzer (falls im Backup vorhanden) --- if (restoredData.blockedUsers) { restoredData.blockedUsers.forEach(user => { if (!currentBlockedUsers.has(user)) { currentBlockedUsers.add(user); changes.users++; } }); } // 7. Daten speichern // --- Wortfilter --- GM_setValue('excludeWords', mergedWords); localStorage.setItem('excludeWords', JSON.stringify(mergedWords)); excludeWords = mergedWords; // --- Händlerfilter --- saveExcludeMerchants(mergedMerchants); // --- Blockierte Benutzer --- const mergedBlockedUsers = Array.from(currentBlockedUsers); GM_setValue('blockedUsers', mergedBlockedUsers); // --- 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); } // 8. UI aktualisieren if (isSettingsOpen) { updateUITheme(); } processArticles(); // 9. Zusammenfassende Meldung erstellen let message = 'Backup wurde erfolgreich wiederhergestellt.\n\nNeue Einträge:'; if (changes.words > 0) { message += `\n• ${changes.words} neue Wörter`; } if (changes.merchants > 0) { message += `\n• ${changes.merchants} neue Händler`; } if (changes.users > 0) { message += `\n• ${changes.users} neue blockierte Benutzer`; } if (changes.words === 0 && changes.merchants === 0 && changes.users === 0) { message += '\nKeine neuen Einträge gefunden.'; } alert(message); } 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('...', '') // Wenn der Händlername CamelCase ist (z.B. FrankfurterRundschau, BerlinerMorgenpost) // erstelle eine Version mit Leerzeichen (z.B. "Frankfurter Rundschau", "Berliner Morgenpost") if (/^[A-Z][a-z]+(?:[A-Z][a-z]+)+$/.test(merchant)) { // Teile den CamelCase Namen an Großbuchstaben const spacedVersion = merchant.split(/(?=[A-Z])/).join(' '); // Prüfe ob die Version mit Leerzeichen im Titel vorkommt const spacedRegex = new RegExp(`\\b${spacedVersion}\\b`, 'i'); if (title.match(spacedRegex)) { title = title.replace(spacedRegex, ''); return title.trim(); } } // NEU: Minimum Länge für Domain-basierte Entfernung const MIN_DOMAIN_LENGTH = 4; if (merchantName.includes('.')) { const domainWithoutExt = merchantName.split('.')[0]; if (domainWithoutExt.length < MIN_DOMAIN_LENGTH) { return title; } } // Spezialfall für eSIM.sm - wir wollen "eSIM" nicht entfernen, da es ein Produktname ist if (merchantName === 'esim.sm' && title.toLowerCase().includes('auf esim')) { return title; } // 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]; } else if (merchantName.includes('.')) { // Extrahiere den Basisnamen aus Domains wie CDKeys.com -> CDKeys baseShopName = merchantName.split('.')[0]; } // Handle special case for any merchant-related pattern in parentheses at beginning if (title.match(/^\([^)]+\)/i)) { const lowerTitle = title.toLowerCase(); const lowerMerchant = merchantName.toLowerCase(); // Check if merchant name is contained in the parentheses at the beginning if (lowerTitle.substring(0, lowerTitle.indexOf(')')).includes(lowerMerchant)) { // Prüfe auf Slash-Format if (title.match(/^\([^/]+\/[^)]+\)/i)) { // Behandle Slash-Format separat return title.replace( new RegExp(`\\(${merchantName}\\s*/\\s*(.+?)\\)`, 'i'), '($1)' ); } return title.replace(/^\([^)]+\)\s*/, ''); } } // 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'), '['); } // Erstelle Domain-Varianten für den Händlernamen und den Basis-Namen const createDomainVariants = (name) => [ `${name}.com`, `${name}.de`, `${name}.co.uk`, `${name}-shop.com`, `${name}-shop.de` ]; const domainVariants = createDomainVariants(merchantName); // Füge auch Domain-Varianten für den Basis-Namen hinzu if (baseShopName !== merchantName) { domainVariants.push(...createDomainVariants(baseShopName)); } // Entferne explizit [Domain]-Muster am Anfang des Titels const handleBracketedDomain = (shopName) => { const escapedName = shopName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); result = result.replace(new RegExp(`\\[${escapedName}\\.com\\]`, 'i'), ''); result = result.replace(new RegExp(`\\[${escapedName}\\.de\\]`, 'i'), ''); result = result.replace(new RegExp(`\\[${escapedName}\\.co\\.uk\\]`, 'i'), ''); }; // Wende auf beide Namen an handleBracketedDomain(merchantName); if (baseShopName !== merchantName) { handleBracketedDomain(baseShopName); } // 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 => { // Spezialfall für Domain-Formate wie "target.com" if (variant.includes('.')) { // Genereller Ansatz, um "target.com" zu entfernen const baseDomain = variant.split('.')[0]; const escapedBase = baseDomain.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); const escapedVariant = variant.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); // Entferne "[domain.com]" Format am Anfang result = result.replace(new RegExp(`\\[${escapedVariant}\\]\\s*`, 'i'), ''); // Entferne auch domänenspezifische Top-Level-Domains - das ist der Teil, der fehlt result = result.replace(new RegExp(`\\[${escapedBase}\\.com\\]\\s*`, 'i'), ''); result = result.replace(new RegExp(`\\[${escapedBase}\\.de\\]\\s*`, 'i'), ''); result = result.replace(new RegExp(`\\[${escapedBase}\\.co\\.uk\\]\\s*`, 'i'), ''); // Spezifisches Pattern für "bei Target.com für" - umfassendere Lösung result = result.replace(new RegExp(`\\s+bei\\s+${escapedBase}\\.com\\s+für`, 'i'), ' für'); // Fallback-Pattern, um alle "bei domain.tld" Formate zu entfernen result = result.replace(new RegExp(`\\s+bei\\s+${escapedBase}\\.[a-z.]+(?:\\s+|$)`, 'i'), ' '); } // Für Namen mit Punkten, verwende spezifischere Ersetzungsmuster const needsSpecialBoundary = variant.includes('.'); const escapedVariant = variant.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); const wordBoundaryPattern = needsSpecialBoundary ? escapedVariant : `\\b${escapedVariant}\\b`; if (needsSpecialBoundary) { } else { // Handle parentheses with multiple entries separated by slashes first const parenthesesPattern = new RegExp( `\\(([^/]*?\\s*)?${escapedVariant}\\s*/\\s*([^)]+)\\)`, 'i' ); if (result.match(parenthesesPattern)) { const match = result.match(parenthesesPattern); const before = match[1] ? match[1].trim() : ''; const after = match[2].trim(); const newContent = [before, after].filter(Boolean).join(' / '); result = result.replace(parenthesesPattern, `(${newContent})`); } // Standard patterns for merchant names without punctuation result = result .replace(new RegExp(`\\s+bei\\s+${wordBoundaryPattern}\\s+`, 'i'), ' ') .replace(new RegExp(`\\s+(?:bei\\s+)?${escapedVariant}(?:[-–]|\\s)*$`, 'i'), '') .replace(new RegExp(`\\s+auf\\s+${escapedVariant}$`, 'i'), '') .replace(new RegExp(`\\s+auf\\s+${escapedVariant}(?=\\s|$)`, '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'), '') // FIX: Händler in eckigen Klammern mit optionalen Leerzeichen .replace(new RegExp(`\\[\\s*${escapedVariant}\\s*\\]\\s*`, 'i'), '') // Händler mit Punkt als Trenner .replace(new RegExp(`^${escapedVariant}\\.\\s*`, 'i'), '') .replace(new RegExp(`\\s+${escapedVariant}\\.\\s+`, 'i'), ' ') .replace(new RegExp(`\\s+${escapedVariant}\\.\\s*$`, 'i'), '') // Händler in Klammern mit fehlenden Klammern .replace(new RegExp(`^${escapedVariant}\\)\\s*`, 'i'), '') .replace(new RegExp(`\\(${escapedVariant}$`, '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; // Add word suggestion list cleanup const suggestionList = document.getElementById('wordSuggestionList'); if (suggestionList) { suggestionList.remove(); } // Close all sub-UIs with state logging if (merchantListDiv?.parentNode) { merchantListDiv.remove(); } if (wordsListDiv?.parentNode) { wordsListDiv.remove(); } if (blockedUsersDiv?.parentNode) { blockedUsersDiv.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 buttonMappings = { merchant: { id: 'showMerchantListButton', html: ' Händlerfilter verwalten' }, words: { id: 'showWordsListButton', html: ' Wortfilter verwalten' }, users: { id: 'showBlockedUsersButton', html: ' Benutzerfilter verwalten' } }; const buttonConfig = buttonMappings[activeSubUI]; if (buttonConfig) { const btn = document.getElementById(buttonConfig.id); if (btn) { btn.innerHTML = buttonConfig.html; 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(); e.stopImmediatePropagation(); if (isSettingsOpen) { if (dealThatOpenedSettings === deal) { cleanup(); } else { cleanup(); dealThatOpenedSettings = deal; createSettingsUI(); } } 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'); hiddenDeals.push(dealId); saveHiddenDeals(); hideDeal(deal); window.removeEventListener('scroll', scrollHandler); } }, true); targetElement.addEventListener('touchend', () => { if (!buttonVisible) { hideButtonContainer.style.display = 'none'; } }, true); } else { targetElement.addEventListener('mouseenter', () => { hideButtonContainer.style.display = 'block'; }, true); targetElement.addEventListener('mouseleave', () => { hideButtonContainer.style.display = 'none'; }, true); hideButton.onclick = (e) => { e.preventDefault(); e.stopPropagation(); const dealId = deal.getAttribute('id'); hiddenDeals.push(dealId); saveHiddenDeals(); hideDeal(deal); return false; }; } hideButtonContainer.appendChild(hideButton); targetElement.appendChild(hideButtonContainer); deal.setAttribute('data-button-added', 'true'); }); } function 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 initializeUI() - Initialisiert Benutzeroberfläche initObserver() - Initialisiert DOM-Beobachter addMerchantPageHideButton() - Fügt Händler-Ausblenden-Button hinzu =================================================================================== */ //#endregion