// ==UserScript==
// @name mydealz Manager
// @namespace http://tampermonkey.net/
// @version 1.15.0
// @description Deals gezielt ausblenden mittels X Button, Filtern nach Händlern und Wörtern im Titel. Teure und kalte Deals ausblenden.
// @author Flo (https://www.mydealz.de/profile/Basics0119) (https://github.com/9jS2PL5T) & Moritz Baumeister (https://www.mydealz.de/profile/BobBaumeister) (https://github.com/grapefruit89)
// @license MIT
// @match https://www.mydealz.de/*
// @match https://www.preisjaeger.at/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=mydealz.de
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// ==/UserScript==
// Versions-Änderungen
// 1.13.0
// ADD: Funktion um den Teilen-Button auszublenden.
// ADD: Warnung bei mehreren aktiven mydealz Manager Versionen.
// ADD: Funktion um Banner & Widgets auszublenden.
// 1.14.0
// ADD: Funktion zum Speichern der Sortierung bei Dealsuche.
// 1.15.0
// ADD: Drag & Drop für SettingsUI
//#region --- 1. Initialisierung und Grundeinstellungen ---
// ===== Konstanten und Konfiguration =====
// --- Storage Keys ---
const VERSION_PREFIX = 'mdm_version_';
const HIDDEN_DEALS_KEY = 'hiddenDeals';
const HIDE_COLD_DEALS_KEY = 'hideColdDeals';
const MAX_PRICE_KEY = 'maxPrice';
const LAST_HIDDEN_DEAL_SHOWN = 'lastHiddenDealShown';
const PREFERRED_SORT_KEY = 'mydealz_preferred_sort';
// --- Selektoren ---
const ARTICLE_SELECTOR = '.thread--deal, .thread--type--list';
const MERCHANT_PAGE_SELECTOR = '.merchant-banner';
// --- System/Performance Konstanten ---
const CLEANUP_TIME = 30000;
const IS_TOUCH_DEVICE = ('ontouchstart' in window) ||
(navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints > 0);
const DEBUG = false;
// --- Feature Flags ---
const FEATURES = {
hideMatchingMerchantNames: 'hideMatchingMerchantNames',
hideShareButtons: 'hideShareButtons',
rememberSort: 'rememberSort'
};
// ===== Instanzerkennung und Cleanup =====
(function detectMultipleInstances() {
const currentVersion = GM_info.script.version;
const now = Date.now();
try {
// Setze Marker für diese Version
const myKey = VERSION_PREFIX + currentVersion;
localStorage.setItem(myKey, now.toString());
// Prüfe auf alle aktiven Versionen
const activeVersions = new Set();
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key?.startsWith(VERSION_PREFIX)) continue;
const version = key.replace(VERSION_PREFIX, '');
const timestamp = parseInt(localStorage.getItem(key) || '0');
if ((now - timestamp) < CLEANUP_TIME) {
activeVersions.add(version);
}
}
if (activeVersions.size > 1) {
const warningMsg = `⚠️ Warnung: Es wurden mehrere Versionen des mydealz Managers gefunden!\n\nAktive Versionen:\n${Array.from(activeVersions).join('\n')}\n\nBitte deaktiviere alle Versionen bis auf eine in deinem Script-Manager.`;
alert(warningMsg);
}
} catch (e) {
console.error('Error in instance detection:', e);
}
})();
// Cleanup beim Entladen der Seite
window.addEventListener('unload', () => {
try {
localStorage.removeItem(VERSION_PREFIX + GM_info.script.version);
} catch (e) {
// Ignoriere Fehler beim Cleanup
}
});
// ===== UI-Ressourcen =====
// --- Font Awesome Einbindung ---
const fontAwesomeLink = document.createElement('link');
fontAwesomeLink.rel = 'stylesheet';
fontAwesomeLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css';
document.head.appendChild(fontAwesomeLink);
// --- Style Elemente ---
const preventAutoCloseStyle = document.createElement('style');
preventAutoCloseStyle.textContent = ``;
document.head.appendChild(preventAutoCloseStyle);
// ===== UI-Konfiguration =====
// --- Titel-Speicher ---
const ORIGINAL_TITLES = new Map();
// --- Sidebar Elemente ---
const SIDEBAR_ELEMENTS = {
banners: {
headerBanners: {
name: "Banner im Header",
selector: '.messages',
storageKey: 'hideHeaderBanners',
hidden: false
},
feedBanners: {
name: "Banner im Feed",
selector: '[id^="customBannerList-id-"], #eventBannerPortal',
storageKey: 'hideFeedBanners',
hidden: false
}
},
widgets: {
topDiscussions: {
name: "Widgets",
selector: '.card.card--type-vertical.listLayout-box.aGrid.card--responsive',
storageKey: 'hideTopDiscussionsWidget',
hidden: false
}
}
};
// ===== Observer-Konfiguration =====
const observer = new MutationObserver(throttle(() => {
processArticles();
addSettingsButton();
addHideButtons();
}, 250));
// ===== UI-Zustand =====
// --- Hauptfenster ---
let isSettingsOpen = false;
let activeSubUI = null;
let dealThatOpenedSettings = null;
// --- UI-Elemente ---
let settingsDiv = null;
let merchantListDiv = null;
let wordsListDiv = null;
let uiClickOutsideHandler = null;
// ===== Filter-Zustand =====
// --- Ausschlusslisten ---
let excludeWords = [];
let excludeMerchantIDs = [];
let hiddenDeals = [];
// --- Filter-Einstellungen ---
let hideColdDeals = localStorage.getItem(HIDE_COLD_DEALS_KEY) === 'true';
let maxPrice = parseFloat(localStorage.getItem(MAX_PRICE_KEY)) || 0;
// ===== Temporäre Daten =====
// --- Vorschläge ---
let suggestedWords = [];
let suggestionClickHandler = null;
// --- Letzte Aktionen ---
let lastHiddenDeal = null; // Speichert nur den letzten ausgeblendeten Deal
let lastHiddenDealShown = false; // Wurde der letzte ausgeblendete Deal bereits angezeigt?
// ===== Menü-Commands =====
// --- Command IDs ---
let menuCommandId;
let merchantCommandId;
let backupCommandId;
let restoreCommandId;
// --- Feature-Flags ---
let hideMatchingMerchantNames = GM_getValue('hideMatchingMerchantNames', false);
let hideCustomBanners = GM_getValue('hideCustomBanners', false);
let hideShareButtons = GM_getValue('hideShareButtons', false);
// --- Temporäre Listen ---
let recentHiddenDeals = [];
//#endregion
//#region --- 2. Hilfsfunktionen (Utility Functions) ---
// ===== HTML & Text Verarbeitung =====
// HTML dekodieren
function decodeHtml(html) {
const txt = document.createElement('textarea');
txt.innerHTML = html;
return txt.value;
}
// Regex-Sonderzeichen escapen
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// ===== Performance Optimierung =====
// Funktion zur Begrenzung der Ausführungshäufigkeit (Throttling)
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
return false;
}, limit);
}
}
}
// Liefert Theme-spezifische Farben basierend auf aktuellem Theme
function getThemeColors() {
// Theme-Erkennung inline
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const htmlElement = document.documentElement;
const bodyElement = document.body;
const isDark =
htmlElement.classList.contains('dark') ||
bodyElement.classList.contains('dark') ||
htmlElement.getAttribute('data-theme') === 'dark' ||
document.querySelector('html[data-theme="dark"]') !== null ||
(prefersDark && !htmlElement.classList.contains('light'));
// Direkt die entsprechenden Farben zurückgeben
return isDark ? THEME_COLORS.dark : THEME_COLORS.light;
}
// === Text-Analyse ===
// Wörter aus Deal-Titel extrahieren
function getWordsFromTitle(deal) {
const titleElement = deal.querySelector('.thread-title a');
if (!titleElement) return [];
const rawTitle = titleElement.querySelector('a')?.getAttribute('title') || titleElement.innerText || '';
const keepWords = ['von', 'der', 'die', 'das', 'bei', 'mit', 'und', 'oder', 'auf', 'für', 'durch', 'bis', 'ab'];
const ignoreWords = [
'Euro', 'EUR', 'VSK', '€', 'VGP', 'cent', 'Cent',
'o.', // oder
'z.B.', 'z.b.', // zum Beispiel
'inkl.', // inklusive
'max.', // maximal
'min.', // minimal
'ca.', // circa
'vs.', // versus
'eff.', // effektiv
'mtl.', // monatlich
'bzw.', // beziehungsweise
'evtl.', // eventuell
'uvm.', // und vieles mehr
'etc.', // et cetera
'zzgl.', // zuzüglich
'Nr.', 'nr.', // Nummer
'St.', 'st.', // Stück
'usw.', // und so weiter
'u.a.', // unter anderem
'u.U.', // unter Umständen
'ggf.', // gegebenenfalls
'p.', // pro/per
];
const ignoreChars = ['&', '+', '!', '-', '/', '%', '–'];
const units = ['MB/s', 'GB/s', 'KB/s', 'Mbit/s', 'Gbit/s', 'Kbit/s'];
const priceContextWords = ['effektiv'];
const specialBrands = ['RTL+'];
const isDate = (word) => {
return /^\d{1,2}[.,]\d{1,2}(?:[.,]\d{2,4})?$/.test(word);
};
const isPriceContext = (word) => {
if (!priceContextWords.includes(word.toLowerCase())) return false;
// Prüfe ob im Titel ein Preis vorkommt
const hasPricePattern = /\d+(?:[.,]\d{2})?(?:€|EUR|Euro)/i;
return hasPricePattern.test(rawTitle);
};
const isPrice = (word) => {
return /^~?\d+(?:[.,]\d{2})?(?:€|EUR)?$/.test(word) ||
/^\d+(?:[.,]\d{2})?(?:\s*cent|\s*Cent)$/i.test(word);
};
const isPercentage = (word) => {
return /^\d+\s*%?$/.test(word) && rawTitle.includes('%');
};
const cleanWord = (word) => {
// Check for special brands first
if (specialBrands.includes(word)) {
return word;
}
// Rest of the existing cleanWord function
if (units.some(unit => word.includes(unit))) {
const cleanedWord = word.trim();
return cleanedWord.replace(/[,;:!?.]+$/, '');
}
return word
.trim()
.replace(/^[^a-zA-Z0-9äöüÄÖÜß]+|[^a-zA-Z0-9äöüÄÖÜß]+$/g, '')
.replace(/^[&+!%–]+$/, '')
.replace(/[-,]+$/, '');
};
const shouldKeepWord = (word) => {
const lowerWord = word.toLowerCase();
if (!word || word.length === 0) return false;
if (ignoreChars.includes(word)) return false;
if (ignoreWords.some(ignore => ignore.toLowerCase() === lowerWord)) return false;
if (isDate(word)) return false;
if (isPrice(word)) return false;
if (isPercentage(word)) return false;
if (isPriceContext(word)) return false;
// Behalte spezielle Wörter
if (keepWords.includes(lowerWord)) return true;
if (units.some(unit => word === unit)) return true;
return true;
};
const splitTitle = (title) => {
// Temporär Einheiten und Abkürzungen schützen
let tempTitle = title;
const replacements = new Map();
// Erst die Einheiten schützen
units.forEach((unit, index) => {
const placeholder = `__UNIT${index}__`;
while (tempTitle.includes(unit)) {
tempTitle = tempTitle.replace(unit, placeholder);
replacements.set(placeholder, unit);
}
});
// Dann die Abkürzungen schützen (z.B. "o." als ganzes Wort)
ignoreWords.forEach((word, index) => {
if (word.includes('.')) {
const placeholder = `__ABBR${index}__`;
// Verbesserte Regex für Abkürzungen, die auch Zahlen berücksichtigt
const regex = new RegExp(`\\b${word.replace('.', '\\.')}\\s*(?=\\d|\\s|$)`, 'g');
while (regex.test(tempTitle)) {
tempTitle = tempTitle.replace(regex, (match) => {
const replacement = ' '; // Ersetze Abkürzung durch Leerzeichen
return replacement;
});
}
}
});
// Split und Platzhalter wiederherstellen
return tempTitle
.split(/[\s\/]+/)
.map(word => {
replacements.forEach((original, placeholder) => {
if (word.includes(placeholder)) {
word = word.replace(placeholder, original);
}
});
return word;
})
.filter(word => word.length > 0); // Entferne leere Strings
};
return splitTitle(rawTitle)
.map(cleanWord)
.filter(shouldKeepWord)
.filter((word, index, self) => self.indexOf(word) === index);
}
//#endregion
//#region --- 3. Datenverwaltung ---
// ===== Einstellungen laden/speichern =====
// Laden aller gespeicherten Einstellungen
function loadSettings() {
// Lade Wortfilter und Händlerfilter
excludeWords = loadExcludeWords();
const merchantsData = loadExcludeMerchants();
excludeMerchantIDs = merchantsData.map(m => m.id);
// Lade Preisfilter
maxPrice = parseFloat(GM_getValue('maxPrice', 0)) || 0;
// Lade UI-Einstellungen
hideColdDeals = GM_getValue('hideColdDeals', false);
hideMatchingMerchantNames = GM_getValue('hideMatchingMerchantNames', false);
window.hideMatchingMerchantNames = hideMatchingMerchantNames;
// Lade Banner-Einstellung und aktualisiere basierend auf individuellen Einstellungen
hideCustomBanners = GM_getValue('hideCustomBanners', false);
// Wenn hideCustomBanners nicht explizit aktiviert ist, prüfe ob alle individuellen
// Elemente ausgeblendet sind und setze hideCustomBanners entsprechend
if (!hideCustomBanners) {
const allHidden = Object.values(SIDEBAR_ELEMENTS).every(element => element.hidden);
if (allHidden && Object.values(SIDEBAR_ELEMENTS).length > 0) {
hideCustomBanners = true;
}
}
window.hideCustomBanners = hideCustomBanners;
// Lade Share Button Einstellung
hideShareButtons = GM_getValue('hideShareButtons', false);
window.hideShareButtons = hideShareButtons;
// Lade versteckte Deals
hiddenDeals = GM_getValue('hiddenDeals', []);
// Lade letzten versteckten Deal
lastHiddenDeal = GM_getValue('lastHiddenDeal', null);
lastHiddenDealShown = GM_getValue(LAST_HIDDEN_DEAL_SHOWN, false);
// Wenn lastHiddenDeal null ist, setze auch lastHiddenDealShown auf true
if (!lastHiddenDeal) {
lastHiddenDealShown = true;
GM_setValue(LAST_HIDDEN_DEAL_SHOWN, true);
}
// Lade Banner-Status
Object.keys(SIDEBAR_ELEMENTS.banners).forEach(key => {
const element = SIDEBAR_ELEMENTS.banners[key];
element.hidden = GM_getValue(element.storageKey, false);
// Sofort Sichtbarkeit anwenden
if (element.hidden) {
document.querySelectorAll(element.selector).forEach(el => {
el.style.display = 'none';
});
}
});
// Lade Sortierungsspeicher-Einstellung
window.rememberSort = GM_getValue('rememberSort', true);
const applyWidgetVisibilityOnce = () => {
Object.keys(SIDEBAR_ELEMENTS.widgets).forEach(key => {
const element = SIDEBAR_ELEMENTS.widgets[key];
element.hidden = GM_getValue(element.storageKey, false);
if (element.hidden) {
document.querySelectorAll(element.selector).forEach(el => {
el.style.display = 'none';
});
}
});
};
// Einmalig ausführen mit Verzögerung für dynamische Elemente
setTimeout(applyWidgetVisibilityOnce, 500);
// UI Updates in einem requestAnimationFrame bündeln
requestAnimationFrame(() => {
updateCustomBannerIcon();
updateSidebarElementsUI();
});
}
const combinedObserver = new MutationObserver(throttle(() => {
// Prozessiere Artikel und UI
processArticles();
addSettingsButton();
addHideButtons();
// Prüfe Widget-Sichtbarkeit
Object.keys(SIDEBAR_ELEMENTS.widgets).forEach(key => {
const element = SIDEBAR_ELEMENTS.widgets[key];
if (element.hidden) {
document.querySelectorAll(element.selector).forEach(el => {
el.style.display = 'none';
});
}
});
}, 250));
// Observer starten
combinedObserver.observe(document.body, {
childList: true,
subtree: true
});
// Storage-Synchronisation zwischen GM und localStorage
async function syncStorage() {
// Prüfe ob Migration bereits durchgeführt wurde
const migrationComplete = GM_getValue('migrationComplete', false);
// Lese Daten aus beiden Speichern
const gmExcludeWords = GM_getValue('excludeWords', null);
const gmExcludeMerchants = GM_getValue('excludeMerchantsData', null);
const gmHiddenDeals = GM_getValue('hiddenDeals', null);
const gmHideColdDeals = GM_getValue('hideColdDeals', null);
const gmMaxPrice = GM_getValue('maxPrice', null);
const lsExcludeWords = JSON.parse(localStorage.getItem('excludeWords') || 'null');
const lsExcludeMerchants = JSON.parse(localStorage.getItem('excludeMerchantsData') || 'null');
const lsHiddenDeals = JSON.parse(localStorage.getItem('hiddenDeals') || 'null');
const lsHideColdDeals = localStorage.getItem('hideColdDeals') || 'null';
const lsMaxPrice = localStorage.getItem('maxPrice') || 'null';
let migrationPerformed = false;
// Migriere Wörter
const effectiveWords = gmExcludeWords || lsExcludeWords || [];
if (effectiveWords.length > 0) {
GM_setValue('excludeWords', effectiveWords);
localStorage.setItem('excludeWords', JSON.stringify(effectiveWords));
excludeWords = effectiveWords;
migrationPerformed = true;
}
// Migriere Händler
const effectiveMerchants = gmExcludeMerchants || lsExcludeMerchants || [];
if (effectiveMerchants.length > 0) {
GM_setValue('excludeMerchantsData', effectiveMerchants);
excludeMerchantIDs = effectiveMerchants.map(m => m.id);
GM_setValue('excludeMerchantIDs', excludeMerchantIDs);
localStorage.setItem('excludeMerchantsData', JSON.stringify(effectiveMerchants));
migrationPerformed = true;
}
// Migriere versteckte Deals
const effectiveHiddenDeals = gmHiddenDeals || lsHiddenDeals || [];
if (effectiveHiddenDeals.length > 0) {
GM_setValue('hiddenDeals', effectiveHiddenDeals);
localStorage.setItem('hiddenDeals', JSON.stringify(effectiveHiddenDeals));
hiddenDeals = effectiveHiddenDeals;
migrationPerformed = true;
}
// Migriere Einstellungen
if (!migrationComplete) {
if (gmHideColdDeals !== null || lsHideColdDeals !== 'null') {
const effectiveHideColdDeals = gmHideColdDeals ?? (lsHideColdDeals === 'true');
GM_setValue('hideColdDeals', effectiveHideColdDeals);
localStorage.setItem('hideColdDeals', effectiveHideColdDeals.toString());
hideColdDeals = effectiveHideColdDeals;
migrationPerformed = true;
}
if (gmMaxPrice !== null || lsMaxPrice !== 'null') {
const effectiveMaxPrice = gmMaxPrice || lsMaxPrice;
GM_setValue('maxPrice', effectiveMaxPrice);
localStorage.setItem('maxPrice', effectiveMaxPrice);
maxPrice = parseFloat(effectiveMaxPrice);
migrationPerformed = true;
}
// rememberSort
const gmRememberSort = GM_getValue('rememberSort', null);
const lsRememberSort = localStorage.getItem('rememberSort') === 'true';
const effectiveRememberSort = gmRememberSort !== null ? gmRememberSort : lsRememberSort;
GM_setValue('rememberSort', effectiveRememberSort);
localStorage.setItem('rememberSort', effectiveRememberSort.toString());
window.rememberSort = effectiveRememberSort;
// hideMatchingMerchantNames
const gmHideMatchingMerchantNames = GM_getValue('hideMatchingMerchantNames', null);
const lsHideMatchingMerchantNames = localStorage.getItem('hideMatchingMerchantNames') === 'true';
const effectiveHideMatchingMerchantNames = gmHideMatchingMerchantNames !== null ?
gmHideMatchingMerchantNames : lsHideMatchingMerchantNames;
GM_setValue('hideMatchingMerchantNames', effectiveHideMatchingMerchantNames);
localStorage.setItem('hideMatchingMerchantNames', effectiveHideMatchingMerchantNames.toString());
window.hideMatchingMerchantNames = effectiveHideMatchingMerchantNames;
// hideShareButtons
const gmHideShareButtons = GM_getValue('hideShareButtons', null);
const lsHideShareButtons = localStorage.getItem('hideShareButtons') === 'true';
const effectiveHideShareButtons = gmHideShareButtons !== null ?
gmHideShareButtons : lsHideShareButtons;
GM_setValue('hideShareButtons', effectiveHideShareButtons);
localStorage.setItem('hideShareButtons', effectiveHideShareButtons.toString());
window.hideShareButtons = effectiveHideShareButtons;
// hideCustomBanners
const gmHideCustomBanners = GM_getValue('hideCustomBanners', null);
const lsHideCustomBanners = localStorage.getItem('hideCustomBanners') === 'true';
const effectiveHideCustomBanners = gmHideCustomBanners !== null ?
gmHideCustomBanners : lsHideCustomBanners;
GM_setValue('hideCustomBanners', effectiveHideCustomBanners);
localStorage.setItem('hideCustomBanners', effectiveHideCustomBanners.toString());
window.hideCustomBanners = effectiveHideCustomBanners;
// Sidebar Elements (Banner & Widgets)
Object.entries(SIDEBAR_ELEMENTS).forEach(([category, items]) => {
Object.entries(items).forEach(([key, element]) => {
const storageKey = element.storageKey;
const gmValue = GM_getValue(storageKey, null);
const lsValue = localStorage.getItem(storageKey) === 'true';
const effectiveValue = gmValue !== null ? gmValue : lsValue;
GM_setValue(storageKey, effectiveValue);
localStorage.setItem(storageKey, effectiveValue.toString());
element.hidden = effectiveValue;
});
});
migrationPerformed = true;
}
// Markiere Migration als abgeschlossen nur wenn tatsächlich Daten migriert wurden
if (migrationPerformed) {
GM_setValue('migrationComplete', true);
}
}
// ===== Wortfilter-Verwaltung =====
// Speichern von Wortfiltern
function saveExcludeWords(words) {
// Normalisiere Groß-/Kleinschreibung und entferne Duplikate
const normalizedWords = words.reduce((acc, word) => {
const lowerWord = word.toLowerCase();
const exists = acc.some(w => w.toLowerCase() === lowerWord);
if (!exists) {
acc.push(word); // Behält originale Schreibweise bei
}
return acc;
}, []);
// Speichere nur die normalisierte Version
GM_setValue('excludeWords', normalizedWords);
localStorage.setItem('excludeWords', JSON.stringify(normalizedWords));
}
// Laden von Wortfiltern
function loadExcludeWords() {
// Load from GM storage
const gmWords = GM_getValue('excludeWords', []);
// Load from localStorage
let lsWords = [];
try {
lsWords = JSON.parse(localStorage.getItem('excludeWords') || '[]');
} catch (e) {
}
// Show final result
const result = gmWords.length > 0 ? gmWords : lsWords;
return result;
}
// ===== Deal-Verwaltung =====
// Speichern ausgeblendeter Deals
function saveHiddenDeals() {
GM_setValue('hiddenDeals', hiddenDeals);
localStorage.setItem('hiddenDeals', JSON.stringify(hiddenDeals));
}
// ===== Händler-Verwaltung =====
// Speichern von Händlerfiltern
function saveExcludeMerchants(merchantsData) {
const validMerchants = merchantsData.filter(m =>
m && typeof m.id !== 'undefined' && m.id !== null &&
typeof m.name !== 'undefined' && m.name !== null
);
const ids = validMerchants.map(m => m.id);
GM_setValue('excludeMerchantIDs', ids);
GM_setValue('excludeMerchantsData', validMerchants);
localStorage.setItem('excludeMerchantsData', JSON.stringify(validMerchants));
excludeMerchantIDs = ids;
}
// Laden von Händlerfiltern
function loadExcludeMerchants() {
const merchantsData = GM_getValue('excludeMerchantsData', []);
const legacyIds = GM_getValue('excludeMerchantIDs', []);
// Filter out invalid entries
const validMerchants = merchantsData.filter(m =>
m &&
typeof m.id !== 'undefined' &&
m.id !== null &&
typeof m.name !== 'undefined' &&
m.name !== null
);
// Convert legacy IDs if needed
if (validMerchants.length === 0 && legacyIds.length > 0) {
return legacyIds
.filter(id => id && typeof id !== 'undefined')
.map(id => ({ id, name: id }));
}
return validMerchants;
}
// ===== Preis-Verwaltung =====
// Speichern des Maximalpreises
function saveMaxPrice(price) {
// Convert to number if it's a string
if (typeof price === 'string') {
price = parseFloat(price.replace(',', '.')) || 0;
}
GM_setValue('maxPrice', price);
localStorage.setItem('maxPrice', price.toString());
maxPrice = price;
}
//#endregion
//#region --- 4. UI-System ---
// ===== Basis UI-Funktionen =====
// Container, Styles, Theme
function initUIContainers() {
settingsDiv = document.createElement('div');
merchantListDiv = document.createElement('div');
wordsListDiv = document.createElement('div');
}
function updateUITheme() {
const colors = getThemeColors();
[settingsDiv, merchantListDiv, wordsListDiv].forEach(div => {
if (div?.parentNode) {
div.style.background = colors.background;
div.style.border = `1px solid ${colors.border}`;
div.style.color = colors.text;
// Update all buttons and inputs
div.querySelectorAll('button:not([id*="close"])').forEach(btn => {
btn.style.background = colors.buttonBg;
btn.style.border = `1px solid ${colors.buttonBorder}`;
btn.style.color = colors.text;
});
div.querySelectorAll('input').forEach(input => {
input.style.background = colors.inputBg;
input.style.border = `1px solid ${colors.border}`;
input.style.color = colors.text;
});
}
});
}
// === UI-Komponenten ===
// --- Dialog-Erstellung ---
function createSettingsUI() {
if (isSettingsOpen) return;
isSettingsOpen = true;
// Lade versteckte Deals neu
const oldHiddenDeals = [...hiddenDeals]; // Copy old state
hiddenDeals = GM_getValue('hiddenDeals', []);
// Lade versteckte Deals neu
hiddenDeals = GM_getValue('hiddenDeals', []);
// Konsistente Überprüfung des "already shown" Status
const wasAlreadyShown = GM_getValue(LAST_HIDDEN_DEAL_SHOWN, false);
// Wenn bereits angezeigt oder kein letzter Deal vorhanden, dann nicht mehr anzeigen
if (!lastHiddenDeal || wasAlreadyShown) {
lastHiddenDealShown = true;
GM_setValue(LAST_HIDDEN_DEAL_SHOWN, true);
} else {
lastHiddenDealShown = false;
// GM_setValue nicht hier setzen, erst beim Schließen des UI
}
// Initialize containers
initUIContainers();
const colors = getThemeColors();
// Get merchant info from current deal
let merchantName = null;
let showMerchantButton = false;
settingsDiv.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 300px;
height: 300px;
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) {
const merchantLink = dealThatOpenedSettings.querySelector('a[data-t="merchantLink"]');
if (merchantLink) {
merchantName = merchantLink.textContent.trim();
showMerchantButton = true;
}
}
// Process articles when opening settings
processArticles();
// Header
const header = document.createElement('div');
header.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 = `
<div class="section-content" style="display: flex; flex-direction: column; gap: 6px;"> <!-- Reduced gap from 8px to 6px -->
<!-- Word Input - mit nativer mydealz Suchfeld-Optik -->
<div style="margin-bottom: 6px;"> <!-- Reduced from 8px to 6px -->
<div style="display: flex; align-items: center; gap: 4px;">
<div class="search-box" style="
flex: 1;
position: relative;
display: flex;
align-items: center;
background: ${colors.inputBg};
border: 1px solid ${colors.border};
border-radius: 8px;
padding: 0 8px;
height: 36px;
transition: all 0.2s ease;
overflow: hidden;
">
<input id="newWordInput"
autocomplete="off"
${IS_TOUCH_DEVICE ? 'readonly' : ''}
placeholder="Neues Wort..."
title="Deals mit hier eingetragenen Wörtern im Titel werden ausgeblendet."
style="
flex: 1;
min-width: 0;
padding: 0;
border: none;
outline: none;
background: transparent;
color: ${colors.text};
font-size: 14px;
height: 100%;
">
</div>
${IS_TOUCH_DEVICE ? `
<button id="enableKeyboardButton" style="...">
<i class="fas fa-keyboard"></i>
</button>
` : ''}
<button id="addWordButton" style="...">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
${showMerchantButton ? `
<button id="hideMerchantButton" style="...">
<i class="fas fa-store-slash"></i> Alle Deals von <span style="font-weight: bold">${merchantName}</span> ausblenden
</button>
` : ''}
${lastHiddenDeal && !lastHiddenDealShown ? `
<div id="lastHiddenDealSection" style="...">
<!-- Last hidden deal content -->
</div>
` : ''}
</div>
`;
// Filter Section
sections.filter.content.innerHTML = `
<div class="section-content" style="display: flex; flex-direction: column; gap: 6px;">
<button id="showWordsListButton" class="menu-button">
<i class="fas fa-list"></i> Wortfilter verwalten
</button>
<!-- Separator -->
<div style="height: 1px; background: ${colors.border}; opacity: 0.3; margin: 4px 0;"></div>
<button id="showMerchantListButton" class="menu-button">
<i class="fas fa-store"></i> Händlerfilter verwalten
</button>
<!-- Separator -->
<div style="height: 1px; background: ${colors.border}; opacity: 0.3; margin: 4px 0;"></div>
<div class="toggle-option" style="display: flex; justify-content: space-between; align-items: center;">
<span title="Deals unter 0° werden ausgeblendet">Kalte Deals</span>
<button type="button" id="hideColdDeals" class="eye-toggle" style="...">
<i class="fas ${hideColdDeals ? 'fa-eye-slash' : 'fa-eye'}"></i>
</button>
</div>
<!-- Separator -->
<div style="height: 1px; background: ${colors.border}; opacity: 0.3; margin: 4px 0;"></div>
<div style="display: flex; align-items: center; gap: 10px;">
<label style="flex-grow: 1" title="Deals über diesem Preis werden ausgeblendet">
Teure Deals
</label>
<input
type="text"
inputmode="decimal"
pattern="[0-9]*[.,]?[0-9]*"
id="settingsMaxPrice"
value="${maxPrice.toLocaleString('de-DE', {
minimumFractionDigits: 0,
maximumFractionDigits: 2
})}"
placeholder="€"
style="
width: 80px;
padding: 4px 8px;
text-align: right;
background: ${colors.inputBg};
border: 1px solid ${colors.border};
border-radius: 3px;
color: ${colors.text};
"
>
</div>
</div>
`;
// 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 = `
<div class="section-content" style="display: flex; flex-direction: column; gap: 6px;">
${document.querySelector('[data-t="shareBtn"]') ? `
<div class="toggle-option" style="display: flex; justify-content: space-between; align-items: center;">
<span title="Blendet den Teilen Button in jedem Deal in der Übersicht aus">Teilen Button</span>
<button type="button" id="hideShareButtons" class="eye-toggle" style="
background: none;
border: none;
cursor: pointer;
font-size: 18px;
color: ${colors.text};
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas ${window.hideShareButtons ? 'fa-eye-slash' : 'fa-eye'}"
aria-label="${window.hideShareButtons ? 'Share Buttons versteckt' : 'Share Buttons sichtbar'}"></i>
</button>
</div>
<!-- Separator -->
<div style="height: 1px; background: ${colors.border}; opacity: 0.3; margin: 6px 0;"></div>
` : ''}
<div class="toggle-option" style="
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
">
<button type="button" id="openSidebarElementsButton" class="text-button" style="
background: none;
border: none;
cursor: pointer;
text-align: left;
padding: 0;
color: ${colors.text};
font-size: inherit;
position: relative;
z-index: 2;
flex-grow: 1;
display: flex;
align-items: center;
" title="${window.innerWidth <= 768 ? 'Verwalte Banner und Widgets' : 'Verwalte Banner und Widgets'}">
${window.innerWidth <= 768 ? 'Banner & Feed-Elemente' : 'Banner & Widgets'}
</button>
<!-- Senkrechter Trennstrich -->
<div style="
width: 1px;
height: 18px;
background-color: ${colors.border};
margin: 0 10px;
opacity: 0.7;
"></div>
<button type="button" id="hideCustomBanners" class="eye-toggle" style="
background: none;
border: none;
cursor: pointer;
font-size: 18px;
color: ${colors.text};
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas ${customBannerIconClass}"
aria-label="${customBannerAriaLabel}"></i>
</button>
</div>
<!-- Separator -->
<div style="height: 1px; background: ${colors.border}; opacity: 0.3; margin: 6px 0;"></div>
<div class="toggle-option" style="display: flex; justify-content: space-between; align-items: center;">
<span style="display: flex; align-items: center; cursor: help;" title="Gibt es zu dem Deal einen hinterlegten Händler und befindet sich der Name des Händlers im Titel, so wird dieser Name aus dem Titel ausgeblendet.">
Händler im Titel
</span>
<button type="button" id="hideMatchingMerchantNames" class="eye-toggle" style="
background: none;
border: none;
cursor: pointer;
font-size: 18px;
color: ${colors.text};
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas ${window.hideMatchingMerchantNames ? 'fa-eye-slash' : 'fa-eye'}"
aria-label="${window.hideMatchingMerchantNames ? 'Händlernamen versteckt' : 'Händlernamen sichtbar'}"></i>
</button>
</div>
<!-- Separator -->
<div style="height: 1px; background: ${colors.border}; opacity: 0.3; margin: 6px 0;"></div>
<!-- Neue Toggle-Option für Sortierung merken (VERSCHOBEN UNTER HÄNDLER IM TITEL) -->
<div class="toggle-option" style="display: flex; justify-content: space-between; align-items: center;">
<span style="display: flex; align-items: center; cursor: help;" title="Standardmäßig wird nach Relevanz sortiert. Wählt der Nutzer eine andere Sortierung, wird diese gespeichert und bei der nächsten Suche automatisch angewandt.">
Sortierung merken
</span>
<button type="button" id="rememberSort" class="toggle-switch" style="
background: none;
border: none;
cursor: pointer;
font-size: 18px;
color: ${colors.text};
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
">
<i class="fas ${window.rememberSort ? 'fa-toggle-on' : 'fa-toggle-off'}"
aria-label="${window.rememberSort ? 'Sortierung wird gespeichert' : 'Sortierung wird nicht gespeichert'}"></i>
</button>
</div>
</div>
`;
// Backup Section
sections.backup.content.innerHTML = `
<div class="section-content" style="display: flex; flex-direction: column; gap: 6px;">
<button id="backupDataButton" class="menu-button">
<i class="fas fa-download"></i> Backup erstellen
</button>
<!-- Separator -->
<div style="height: 1px; background: ${colors.border}; opacity: 0.3; margin: 4px 0;"></div>
<button id="restoreDataButton" class="menu-button">
<i class="fas fa-upload"></i> Backup wiederherstellen
</button>
<input type="file"
id="restoreFileInput"
accept=".json"
style="display: none;">
</div>
`;
// 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 = `
<button id="closeSettingsButton" class="button button--type-secondary button--mode-default button--shape-circle">
<span>Schließen</span>
</button>
`;
// 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);
if (DEBUG) console.log('[MDM Sort] Gespeicherte Sortierung gelöscht (Feature deaktiviert)');
}
// Update icon
const icon = e.currentTarget.querySelector('i');
if (icon) {
icon.className = `fas ${newState ? 'fa-toggle-on' : 'fa-toggle-off'}`;
icon.setAttribute('aria-label', newState ? 'Sortierung wird gespeichert' : 'Sortierung wird nicht gespeichert');
}
});
// Accordion section styling - reduce padding and margins
const accordionSectionStyle = document.createElement('style');
accordionSectionStyle.textContent = `
.accordion-section {
margin-bottom: 4px;
}
.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();
}
}
});
}
// 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();
}
});
}
// 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();
// Deal als angezeigt markieren
lastHiddenDealShown = true;
GM_setValue(LAST_HIDDEN_DEAL_SHOWN, true);
// UI aktualisieren
document.getElementById('lastHiddenDealSection').style.display = 'none';
// Finde den wiederhergestellten Deal
const restoredDeal = document.getElementById(dealId);
if (restoredDeal) {
// Hide-Button Container zurücksetzen
const hideButtonContainer = restoredDeal.querySelector('.cept-vote-temp div');
if (hideButtonContainer) {
hideButtonContainer.style.display = 'none';
}
}
// Deals neu verarbeiten
processArticles();
});
// Setup ClickOutsideHandler für das Settings-UI
setupClickOutsideHandler();
document.getElementById('hideShareButtons')?.addEventListener('click', async (e) => {
// Toggle state
const newState = !window.hideShareButtons;
window.hideShareButtons = newState;
GM_setValue('hideShareButtons', newState);
// Update icon
const icon = e.currentTarget.querySelector('i');
if (icon) {
icon.className = newState ? 'fas fa-eye-slash' : 'fas fa-eye';
icon.setAttribute('aria-label', newState ? 'Share Buttons versteckt' : 'Share Buttons sichtbar');
}
// Update visibility
updateShareButtonsVisibility(newState);
await Promise.resolve();
processArticles();
});
document.getElementById('hideMatchingMerchantNames')?.addEventListener('click', async (e) => {
// Toggle state
const newState = !window.hideMatchingMerchantNames;
window.hideMatchingMerchantNames = newState;
GM_setValue('hideMatchingMerchantNames', newState);
// Update icon
const icon = e.currentTarget.querySelector('i');
if (icon) {
icon.className = `fas ${newState ? 'fa-eye-slash' : 'fa-eye'}`;
icon.setAttribute('aria-label', newState ? 'Händlernamen versteckt' : 'Händlernamen sichtbar');
}
// Update visibility
await Promise.resolve();
processArticles();
});
// Event Handler für den Custom Banner-Toggle
document.getElementById('hideCustomBanners')?.addEventListener('click', async () => {
// Toggle state
const newState = !window.hideCustomBanners;
// Aktualisiere alle SIDEBAR_ELEMENTS nur im Status
Object.keys(SIDEBAR_ELEMENTS).forEach(key => {
const element = SIDEBAR_ELEMENTS[key];
element.hidden = newState;
GM_setValue(element.storageKey, newState);
});
// Aktualisiere die Haupt-Variable
window.hideCustomBanners = newState;
GM_setValue('hideCustomBanners', newState);
// Haupt-Icon aktualisieren
updateCustomBannerIcon();
// WICHTIG: Aktualisiere auch die UI-Elemente in der Sidebar
updateSidebarElementsUI();
});
document.getElementById('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
if (wordsListDiv?.parentNode || merchantListDiv?.parentNode || document.getElementById('sidebarElementsDiv')) {
const settingsRect = element.getBoundingClientRect();
[
wordsListDiv,
merchantListDiv,
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,
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,
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 += `
<div class="sidebar-category" style="margin-bottom: 15px;">
<h4 style="margin: 0 0 8px 0; font-size: 14px;">${category.title}</h4>
<div style="display: flex; flex-direction: column; gap: 6px;">
`;
category.keys.forEach(key => {
// Prüfe ob es ein Banner oder Widget Element ist
const element = SIDEBAR_ELEMENTS[catKey === 'banner' ? 'banners' : 'widgets'][key];
if (!element) return;
const isActive = !element.hidden;
elementsHTML += `
<div class="sidebar-element" style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
background: ${colors.itemBg};
border: 1px solid ${colors.border};
border-radius: 3px;
">
<span style="font-size: 14px;">${element.name}</span>
<button class="toggle-sidebar-element" data-key="${key}" data-type="${catKey}" style="
background: none;
border: none;
cursor: pointer;
font-size: 16px;
color: ${colors.text};
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
">
<i class="fas ${isActive ? 'fa-eye' : 'fa-eye-slash'}"
aria-label="${isActive ? 'Element sichtbar' : 'Element versteckt'}"></i>
</button>
</div>
`;
});
elementsHTML += `
</div>
</div>
`;
});
sidebarElementsDiv.innerHTML = `
<div style="margin-bottom: 15px;">
${elementsHTML}
</div>
<div style="
display: flex;
justify-content: center;
margin-top: 20px;
">
<button id="closeSidebarElementsButton" class="button button--type-secondary button--mode-default button--shape-circle" style="
padding: 8px 16px;
display: inline-block;
width: auto;
min-width: 100px;
text-align: center;
font-size: 14px;
">
<span>Schließen</span>
</button>
</div>
`;
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 = `
<div class="accordion-header" style="
display: flex;
align-items: center;
padding: 10px;
cursor: pointer;
user-select: none;
background: ${colors.background};
border-bottom: 1px solid ${colors.border};
position: relative;
z-index: 1;
">
<i class="fas fa-${iconName}" style="margin-right: 10px;"></i>
<span>${title}</span>
<i class="fas fa-chevron-down" style="margin-left: auto; transition: transform 0.3s"></i>
</div>
<div class="accordion-content" style="
display: none;
padding: 10px;
position: relative;
z-index: 0;
border-bottom: 1px solid ${colors.border};
">
<div class="accordion-inner" style="
display: flex;
flex-direction: column;
gap: 12px;
">
</div>
</div>
`;
// 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 => `
<div class="merchant-item" style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
padding: 5px;
background: ${colors.itemBg};
color: ${colors.text};
border: 1px solid ${colors.border};
border-radius: 3px;">
<span>${merchant.name}</span>
<button class="delete-merchant" data-id="${merchant.id}" style="
background: none;
border: none;
cursor: pointer;
color: ${colors.text};">
<i class="fas fa-times"></i>
</button>
</div>
`).join('');
merchantListDiv.innerHTML = `
<h4 style="margin-bottom: 10px;">Ausgeblendete Händler (${currentMerchants.length})</h4>
<input type="text" id="merchantSearch" placeholder="Händler suchen..."
style="
width: 100%;
padding: 5px;
margin-bottom: 10px;
background: ${colors.inputBg};
border: 1px solid ${colors.border};
color: ${colors.text};
border-radius: 3px;">
<div style="margin-bottom: 15px;">
<div id="merchantList" style="
margin-bottom: 10px;
height: 200px;
overflow-y: auto;
padding-right: 5px;
min-height: 200px;">
${merchantListHTML}
</div>
<button id="clearMerchantListButton" style="
width: 100%;
padding: 5px 10px;
background: ${colors.buttonBg};
border: 1px solid ${colors.buttonBorder};
color: ${colors.text};
border-radius: 3px;
cursor: pointer;
margin-top: 10px;">
<i class="fas fa-trash"></i> Alle Händler entfernen
</button>
</div>
<div style="
margin-top: 20px;
display: flex;
justify-content: center;
">
<button id="closeMerchantListButton" class="button button--type-secondary button--mode-default button--shape-circle">
<span>Schließen</span>
</button>
</div>
`;
// 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 => `
<div class="word-item" style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
padding: 5px;
background: ${colors.itemBg};
color: ${colors.text};
border: 1px solid ${colors.border};
border-radius: 3px;">
<span style="word-break: break-word;">${word}</span>
<button class="delete-word" data-word="${word}" style="
background: none;
border: none;
cursor: pointer;
color: ${colors.text};">
<i class="fas fa-times"></i>
</button>
</div>
`).join('');
wordsListDiv.innerHTML = `
<h4 style="margin-bottom: 10px;">Ausgeblendete Wörter (${currentWords.length})</h4>
<input type="text" id="wordSearch" placeholder="Wörter suchen..."
style="
width: 100%;
padding: 5px;
margin-bottom: 10px;
background: ${colors.inputBg};
border: 1px solid ${colors.border};
color: ${colors.text};
border-radius: 3px;">
<div style="margin-bottom: 15px;">
<div id="wordsList" style="
margin-bottom: 10px;
height: 200px;
overflow-y: auto;
padding-right: 5px;
min-height: 200px;">
${wordsListHTML}
</div>
<button id="clearWordsListButton" style="
width: 100%;
padding: 5px 10px;
background: ${colors.buttonBg};
border: 1px solid ${colors.buttonBorder};
color: ${colors.text};
border-radius: 3px;
cursor: pointer;
margin-top: 10px;">
<i class="fas fa-trash"></i> Alle Wörter entfernen
</button>
</div>
<div style="
margin-top: 20px;
display: flex;
justify-content: center;
">
<button id="closeWordsListButton" class="button button--type-secondary button--mode-default button--shape-circle">
<span>Schließen</span>
</button>
</div>
`;
// Add the div to the document body
document.body.appendChild(wordsListDiv);
setupClickOutsideHandler();
// Nach dem Hinzufügen zum DOM den Button anpassen
document.getElementById('closeWordsListButton').style.cssText = `
padding: 8px 16px;
display: inline-block;
width: auto;
min-width: 100px;
text-align: center;
`;
// Add search functionality
const searchInput = document.getElementById('wordSearch');
searchInput.addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
let visibleCount = 0;
// Hole aktuelle Wörter statt die ursprüngliche Liste zu verwenden
const currentWords = loadExcludeWords();
const totalCount = currentWords.length;
document.querySelectorAll('.word-item').forEach(item => {
const word = item.querySelector('span').textContent.toLowerCase();
const isVisible = word.includes(searchTerm);
item.style.display = isVisible ? 'flex' : 'none';
if (isVisible) visibleCount++;
});
// Update heading counter
const heading = wordsListDiv.querySelector('h4');
if (heading) {
heading.textContent = searchTerm
? `Ausgeblendete Wörter (${visibleCount}/${totalCount})`
: `Ausgeblendete Wörter (${totalCount})`;
}
});
// Alle Wörter entfernen Button
document.getElementById('clearWordsListButton').addEventListener('click', () => {
if (confirm('Möchten Sie wirklich alle Wörter aus der Liste entfernen?')) {
saveExcludeWords([]);
document.getElementById('wordsList').innerHTML = '';
excludeWords = [];
processArticles();
// Immediately update counter in heading
const heading = wordsListDiv.querySelector('h4');
if (heading) {
heading.textContent = 'Ausgeblendete Wörter (0)';
}
}
});
// Add delete handlers
document.querySelectorAll('.delete-word').forEach(button => {
button.addEventListener('click', function(e) {
handleWordDelete(e);
});
});
// Update close button handlers in createExcludeWordsUI
document.getElementById('closeWordsListButton').addEventListener('click', (e) => {
e.stopPropagation(); // Prevent event bubbling
closeActiveSubUI();
});
// Before adding to DOM
document.body.appendChild(wordsListDiv);
setupClickOutsideHandler();
}
// === UI-Updates ===
// --- Listen & Status ---
// Liste der Händler/Wörter aktualisieren
function updateActiveLists() {
const colors = getThemeColors();
if (activeSubUI === 'merchant' && merchantListDiv) {
const merchantList = document.getElementById('merchantList');
if (merchantList) {
const currentMerchants = loadExcludeMerchants();
merchantList.innerHTML = currentMerchants.map(merchant => `
<div class="merchant-item" style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
padding: 5px;
background: ${colors.itemBg};
color: ${colors.text};
border: 1px solid ${colors.border};
border-radius: 3px;">
<div style="display: flex; flex-direction: column;">
<span>${merchant.name}</span>
<span style="color: ${colors.text}; opacity: 0.7; font-size: 0.8em;">ID: ${merchant.id}</span>
</div>
<button class="delete-merchant" data-id="${merchant.id}" style="
background: none;
border: none;
cursor: pointer;
color: ${colors.text};">
<i class="fas fa-times"></i>
</button>
</div>
`).join('');
// Event Listener neu hinzufügen
document.querySelectorAll('.delete-merchant').forEach(button => {
button.addEventListener('click', function(e) {
handleMerchantDelete(e);
});
});
}
} else if (activeSubUI === 'words' && wordsListDiv) {
const wordsList = document.getElementById('wordsList');
if (wordsList) {
const currentWords = loadExcludeWords();
wordsList.innerHTML = currentWords.map(word => `
<div class="word-item" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: #f0f0f0; border-radius: 3px;">
<span style="word-break: break-word;">${word}</span>
<button class="delete-word" data-word="${word}" style="background: none; border: none; cursor: pointer; color: #666;">
<i class="fas fa-times"></i>
</button>
</div>
`).join('');
// Event Listener neu hinzufügen
document.querySelectorAll('.delete-word').forEach(button => {
button.addEventListener('click', handleWordDelete);
});
}
}
}
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 => `
<div class="word-suggestion-item" style="padding: 10px; border-bottom: 1px solid #eee; cursor: pointer; transition: background-color 0.2s;">
${word}
</div>
`).join('');
document.body.appendChild(wordSuggestionList);
wordSuggestionList.scrollTop = scrollPosition;
// Add event listeners for items
wordSuggestionList.querySelectorAll('.word-suggestion-item').forEach(item => {
item.addEventListener('mouseenter', () => {
item.style.backgroundColor = colors.itemBg;
});
item.addEventListener('mouseleave', () => {
item.style.backgroundColor = colors.background;
});
item.addEventListener('click', handleWordSelection);
});
// 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 sidebarElementsOpen = document.getElementById('sidebarElementsDiv')?.parentNode;
// Check if click was outside all UIs
const clickedOutside = (!settingsOpen || !settingsDiv.contains(e.target)) &&
(!merchantsOpen || !merchantListDiv.contains(e.target)) &&
(!wordsOpen || !wordsListDiv.contains(e.target)) &&
(!sidebarElementsOpen || !document.getElementById('sidebarElementsDiv').contains(e.target));
if (clickedOutside) {
cleanup();
// Explicit cleanup of UI elements
if (settingsDiv?.parentNode) settingsDiv.remove();
if (merchantListDiv?.parentNode) merchantListDiv.remove();
if (wordsListDiv?.parentNode) wordsListDiv.remove();
// Sicherer Zugriff mit optionaler Verkettung
const sidebarDiv = document.getElementById('sidebarElementsDiv');
if (sidebarDiv?.parentNode) sidebarDiv.remove();
// Reset states
isSettingsOpen = false;
activeSubUI = null;
// Remove handler
document.removeEventListener('click', uiClickOutsideHandler);
uiClickOutsideHandler = null;
}
};
// Add with delay to prevent immediate trigger
setTimeout(() => {
document.addEventListener('click', uiClickOutsideHandler);
}, 100);
}
function createSuggestionClickHandler() {
// Remove old handler if exists
if (suggestionClickHandler) {
document.removeEventListener('click', suggestionClickHandler);
}
suggestionClickHandler = (e) => {
const list = document.getElementById('wordSuggestionList');
const input = document.getElementById('newWordInput');
if (!list?.contains(e.target) && !input?.contains(e.target)) {
list?.remove();
}
};
document.addEventListener('click', suggestionClickHandler);
return suggestionClickHandler;
}
function handleWordSelection(e) {
e.preventDefault();
e.stopPropagation();
const wordSuggestionList = document.getElementById('wordSuggestionList');
const scrollPosition = wordSuggestionList.scrollTop; // Save scroll position
const word = e.target.textContent.trim();
const newWordInput = document.getElementById('newWordInput');
const currentValue = newWordInput.value.trim();
newWordInput.value = currentValue ? `${currentValue} ${word}` : word;
suggestedWords = suggestedWords.filter(w => w !== word);
updateSuggestionList();
newWordInput.focus();
// Restore scroll position after list update
const updatedList = document.getElementById('wordSuggestionList');
if (updatedList) {
updatedList.scrollTop = scrollPosition;
}
}
function setupScrollHandling() {
let isScrollingUI = false;
let lastActiveUI = null;
let touchStartY = 0;
// Hilfsfunktion zum Prüfen ob ein Element scrollbar ist
const isScrollable = (element) => {
return element.scrollHeight > element.clientHeight;
};
// Hilfsfunktion zum Prüfen ob ein Element am Anfang/Ende des Scrollbereichs ist
const isAtScrollLimit = (element, delta) => {
if (delta > 0) {
return element.scrollTop + element.clientHeight >= element.scrollHeight - 1;
} else {
return element.scrollTop <= 0;
}
};
function handleScroll(e) {
// Prüfe ob der Mauszeiger über einem UI-Element ist
const isOverUI = e.target.closest('#mdm-settings-popup, #merchantListDiv, #wordsListDiv, #wordSuggestionList, #sidebarElementsDiv') ||
settingsDiv?.contains(e.target) ||
merchantListDiv?.contains(e.target) ||
wordsListDiv?.contains(e.target) ||
document.getElementById('sidebarElementsDiv')?.contains(e.target);
if (isOverUI) {
// Verhindern des Standard-Scroll-Verhaltens (Website-Scrolling)
e.preventDefault();
// Finde das scrollbare übergeordnete Element
const scrollableContainer = e.target.closest('#mdm-settings-content, #merchantList, #wordsList, #wordSuggestionList, #sidebarElementsDiv');
if (scrollableContainer && isScrollable(scrollableContainer)) {
// Manuelles Scrollen des Containers implementieren
const deltaY = e.deltaY || e.detail || e.wheelDelta;
const scrollAmount = deltaY > 0 ? 40 : -40; // Scroll-Schrittgröße
// Scrolle den Container
scrollableContainer.scrollTop += scrollAmount;
}
// Event-Propagation stoppen
e.stopPropagation();
return false;
}
}
// Event-Listener für das Mausrad mit passiver Option auf false (damit preventDefault funktioniert)
document.addEventListener('wheel', handleScroll, { passive: false });
function handleTouchStart(e) {
const touch = e.touches[0];
touchStartY = touch.clientY;
const uiElements = [
settingsDiv,
merchantListDiv,
wordsListDiv,
document.getElementById('wordSuggestionList'),
document.getElementById('sidebarElementsDiv')
];
isScrollingUI = uiElements.some(el => {
if (!el?.parentNode) return false;
const rect = el.getBoundingClientRect();
return touch.clientX >= rect.left &&
touch.clientX <= rect.right &&
touch.clientY >= rect.top &&
touch.clientY <= rect.bottom;
});
}
function handleTouchMove(e) {
if (!isScrollingUI) return;
const touch = e.touches[0];
const scrollableElement = e.target.closest('#mdm-settings-content, #merchantList, #wordsList, #sidebarElementsDiv');
if (scrollableElement && isScrollable(scrollableElement)) {
const deltaY = touchStartY - touch.clientY;
// Immer preventDefault aufrufen, um Seiten-Scrolling zu verhindern
e.preventDefault();
// Scrollen des UI-Elements
scrollableElement.scrollTop += deltaY;
} else {
// Blockiere Scrollen außerhalb der Listen
e.preventDefault();
}
touchStartY = touch.clientY;
}
function handleMouseEnter() {
isScrollingUI = true;
lastActiveUI = this;
}
function handleMouseLeave() {
isScrollingUI = false;
lastActiveUI = null;
}
function setupUIElement(element) {
if (!element?.parentNode) return;
element.addEventListener('mouseenter', handleMouseEnter);
element.addEventListener('mouseleave', handleMouseLeave);
}
function setupAllElements() {
// Füge auch sidebarElementsDiv zu den zu überwachenden Elementen hinzu
[
settingsDiv,
merchantListDiv,
wordsListDiv,
document.getElementById('wordSuggestionList'),
document.getElementById('sidebarElementsDiv')
].forEach(setupUIElement);
}
// Initial Setup
setupAllElements();
// Event Listener
if (IS_TOUCH_DEVICE) {
document.addEventListener('touchstart', handleTouchStart, { passive: true });
document.addEventListener('touchmove', handleTouchMove, { passive: false });
}
// MutationObserver für dynamisch hinzugefügte UIs
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length) {
// Prüfe auf neu hinzugefügtes Seitenleisten-UI
const addedSidebarUI = Array.from(mutation.addedNodes).find(
node => node.id === 'sidebarElementsDiv'
);
if (addedSidebarUI) {
setupUIElement(addedSidebarUI);
}
}
});
setupAllElements();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Cleanup-Funktion
return () => {
if (IS_TOUCH_DEVICE) {
document.removeEventListener('touchstart', handleTouchStart);
document.removeEventListener('touchmove', handleTouchMove);
}
document.removeEventListener('wheel', handleScroll);
[
settingsDiv,
merchantListDiv,
wordsListDiv,
document.getElementById('wordSuggestionList'),
document.getElementById('sidebarElementsDiv')
].forEach(el => {
if (el?.parentNode) {
el.removeEventListener('mouseenter', handleMouseEnter);
el.removeEventListener('mouseleave', handleMouseLeave);
}
});
observer.disconnect();
};
}
// --- Delete-Operationen ---
function handleWordDelete(e) {
e.preventDefault();
e.stopPropagation();
const deleteButton = e.target.closest('.delete-word');
if (!deleteButton) return;
const wordToDelete = deleteButton.dataset.word;
const wordItem = deleteButton.closest('.word-item');
// Update excludeWords array
excludeWords = excludeWords.filter(word => word !== wordToDelete);
saveExcludeWords(excludeWords);
// Get search state and counts
const searchInput = document.getElementById('wordSearch');
const searchTerm = searchInput?.value.trim().toLowerCase();
const totalItems = document.querySelectorAll('.word-item').length;
// Calculate visible items for search
let visibleCount = 0;
if (searchTerm) {
const visibleItems = Array.from(document.querySelectorAll('.word-item')).filter(item => {
const itemWord = item.querySelector('span').textContent.toLowerCase();
const isVisible = itemWord.includes(searchTerm) && itemWord !== wordToDelete.toLowerCase();
return isVisible;
});
visibleCount = visibleItems.length;
}
// Remove item and update UI
wordItem.remove();
processArticles();
// Update counter in heading
const heading = wordsListDiv.querySelector('h4');
if (heading) {
const newTotal = totalItems - 1;
heading.textContent = searchTerm
? `Ausgeblendete Wörter (${visibleCount}/${newTotal})`
: `Ausgeblendete Wörter (${newTotal})`;
}
}
// Händler-Löschung Handler
function handleMerchantDelete(e) {
e.preventDefault();
e.stopPropagation();
const deleteButton = e.target.closest('.delete-merchant');
if (!deleteButton) return;
const idToDelete = deleteButton.dataset.id;
const merchantItem = deleteButton.closest('.merchant-item');
// Update merchants array
const merchantsData = loadExcludeMerchants();
const updatedMerchants = merchantsData.filter(m => m.id !== idToDelete);
saveExcludeMerchants(updatedMerchants);
// Get search state and counts
const searchInput = document.getElementById('merchantSearch');
const searchTerm = searchInput?.value.trim().toLowerCase();
const totalItems = document.querySelectorAll('.merchant-item').length;
// Calculate visible items for search
let visibleCount = 0;
if (searchTerm) {
const visibleItems = Array.from(document.querySelectorAll('.merchant-item')).filter(item => {
const merchantName = item.querySelector('span').textContent.toLowerCase();
const isVisible = merchantName.includes(searchTerm) &&
item.querySelector('.delete-merchant').dataset.id !== idToDelete;
return isVisible;
});
visibleCount = visibleItems.length;
}
// Remove item and update UI
merchantItem.remove();
processArticles();
// Update counter in heading
const heading = merchantListDiv.querySelector('h4');
if (heading) {
const newTotal = totalItems - 1;
heading.textContent = searchTerm
? `Ausgeblendete Händler (${visibleCount}/${newTotal})`
: `Ausgeblendete Händler (${newTotal})`;
}
}
// === Layout & UI-Hilfen ===
// Sub-UI Position berechnen
function getSubUIPosition() {
const 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 = `
<span style="font-weight: bold;">${merchant.name}</span>
<button class="delete-merchant" data-id="${merchant.id}" style="background: none; border: none; cursor: pointer; color: #666;">
<i class="fas fa-times"></i>
</button>
`;
// Insert at beginning of list
merchantList.insertBefore(div, merchantList.firstChild);
}
// 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'
? '<i class="fas fa-times"></i> Händlerfilter schließen'
: '<i class="fas fa-store"></i> Händlerfilter verwalten';
}
if (wordsButton) {
wordsButton.innerHTML = activeSubUI === 'words'
? '<i class="fas fa-times"></i> Wortfilter schließen'
: '<i class="fas fa-list"></i> 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();
}
// Reset button texts
const merchantButton = document.getElementById('showMerchantListButton');
const wordsButton = document.getElementById('showWordsListButton');
if (merchantButton) {
merchantButton.innerHTML = '<i class="fas fa-store"></i> Händlerfilter verwalten';
merchantButton.removeAttribute('data-processing');
}
if (wordsButton) {
wordsButton.innerHTML = '<i class="fas fa-list"></i> Wortfilter verwalten';
}
activeSubUI = null;
}
//#endregion
//#region --- 5. Deal-Verarbeitung ---
// ===== Hauptfunktionen =====
// Artikel verarbeiten und filtern
function processArticles() {
// Am Anfang der Funktion den aktuellen Status aus Storage laden
window.hideCustomBanners = GM_getValue('hideCustomBanners', false);
const processedDeals = new Set();
const articles = document.querySelectorAll('article.thread--deal, article.thread--voucher');
let hiddenCount = 0;
articles.forEach(article => {
// Debug: Test if shouldExcludeArticle is being called
const shouldExclude = shouldExcludeArticle(article);
if (shouldExclude) {
// FIX: Sicherstellen, dass die Anzeige wirklich auf 'none' gesetzt wird
article.style.display = 'none';
article.setAttribute('data-hidden-by-mydealz-manager', 'true'); // Markieren für Debug-Zwecke
hiddenCount++;
} else {
// Sicherstellen, dass der Artikel sichtbar ist
article.style.display = '';
article.removeAttribute('data-hidden-by-mydealz-manager');
}
});
document.querySelectorAll('article.thread--type-list').forEach(deal => {
// Korrigierter Selektor für die Temperatur
const tempElement = deal.querySelector('.cept-vote-temp span');
const temperatureText = tempElement ? tempElement.textContent.trim() : null;
// Extrahiere nur die Zahl aus dem Text
const temperatureMatch = temperatureText ? temperatureText.match(/([-+]?\d+)°/) : null;
const temperature = temperatureMatch ? parseInt(temperatureMatch[1]) : null;
const isCold = temperature !== null && temperature < 0;
// Markiere das Element für bessere Sichtbarkeit im Debug
deal.dataset.cold = isCold ? 'true' : 'false';
if (hideColdDeals && isCold) {
deal.style.display = 'none';
} else {
deal.style.display = ''; // Reset display
}
});
// Bestehende Deal-Verarbeitung
const deals = document.querySelectorAll('article.thread--deal, article.thread--voucher');
deals.forEach(deal => {
const dealId = deal.getAttribute('id');
// Skip wenn bereits verarbeitet
if (processedDeals.has(dealId)) return;
processedDeals.add(dealId);
if (hiddenDeals.includes(dealId)) {
hideDeal(deal);
return;
}
if (shouldExcludeArticle(deal)) {
hideDeal(deal);
return;
}
deal.style.display = 'block';
deal.style.opacity = '1';
// Custom Banner Handling
if (window.hideCustomBanners) {
Object.entries(SIDEBAR_ELEMENTS).forEach(([category, items]) => {
Object.entries(items).forEach(([key, element]) => {
const elements = document.querySelectorAll(element.selector);
elements.forEach(el => {
const oldDisplay = el.style.display;
el.style.display = element.hidden ? 'none' : '';
});
});
});
}
// Händlername im Titel
if (window.hideMatchingMerchantNames) {
const titleElement = deal.querySelector('.thread-title');
const merchantLink = deal.querySelector('a[data-t="merchantLink"]');
if (titleElement && merchantLink) {
const merchantName = merchantLink.textContent.trim();
const titleLink = titleElement.querySelector('a');
if (titleLink) {
// Speichere ursprünglichen Titel falls noch nicht geschehen
if (!ORIGINAL_TITLES.has(dealId)) {
ORIGINAL_TITLES.set(dealId, titleLink.textContent);
}
const originalTitle = ORIGINAL_TITLES.get(dealId) || titleLink.textContent;
const newTitle = removeMerchantNameFromTitle(originalTitle, merchantName);
titleLink.textContent = newTitle;
}
}
} else {
// Originaltitel wiederherstellen falls vorhanden
const titleLink = deal.querySelector('.thread-title a');
const dealId = deal.getAttribute('id');
if (titleLink && ORIGINAL_TITLES.has(dealId)) {
titleLink.textContent = ORIGINAL_TITLES.get(dealId);
}
}
});
// Sortierungsspeicher verarbeiten
processSortRemembering();
}
// ===== Filterlogik =====
// Ausschlussprüfung für Artikel
function shouldExcludeArticle(article) {
const titleElement = article.querySelector('.thread-title');
if (!titleElement) return false;
// 2. Quick checks (temperature & price)
// Temperature check
if (hideColdDeals) {
const tempElement = article.querySelector('.cept-vote-temp .overflow--wrap-off');
if (tempElement) {
const temp = parseInt(tempElement.textContent);
if (!isNaN(temp) && temp < 0) return true;
}
}
const dealId = article.getAttribute('id');
// Price check
if (maxPrice > 0) {
const priceSelectors = ['.thread-price', '.text--color-greyShade'];
for (const selector of priceSelectors) {
const priceElement = article.querySelector(selector);
if (priceElement) {
const priceText = priceElement.textContent.trim();
// Ignore percentage discounts
if (priceText.includes('%')) {
continue;
}
// Ignore negative values (e.g., "-20€")
if (priceText.startsWith('-')) {
continue;
}
// Check if the text contains a price in € format
if (!priceText.includes('€')) {
continue;
}
// Extract numeric price - handle European format (47.170,00€)
const euroFormatMatch = priceText.match(/([\d.,]+)\s*€/);
if (euroFormatMatch) {
let extractedPrice = euroFormatMatch[1];
// Remove all dots first (thousand separators)
extractedPrice = extractedPrice.replace(/\./g, '');
// Then replace comma with dot for decimal
extractedPrice = extractedPrice.replace(',', '.');
// Hier ist die wichtige Änderung: const hinzufügen
const priceValue = parseFloat(extractedPrice);
if (!isNaN(priceValue) && priceValue > maxPrice) {
return true;
}
}
}
}
}
// 3. Complex checks
// Get title text
const rawTitle = titleElement.querySelector('a')?.getAttribute('title') || titleElement.innerText;
// Hilfsfunktion für ß zu ss und ue zu ü Konvertierung
function normalizeGerman(text) {
return text.toLowerCase()
.replace(/ß/g, 'ss')
.replace(/ue/g, 'ü');
}
// Normalisiere den Titel
const processedTitle = normalizeGerman(rawTitle);
// Check excludeWords
if (excludeWords.some(word => {
const searchTerm = normalizeGerman(word);
const lowerTitle = normalizeGerman(processedTitle);
// Handle words in brackets like [CB]
if (searchTerm.startsWith('[') && searchTerm.endsWith(']')) {
const match = lowerTitle.includes(searchTerm);
return match;
}
// Handle words with special characters (+)
if (searchTerm.includes('+')) {
const match = lowerTitle.includes(searchTerm);
return match;
}
// Handle multi-word phrases
if (searchTerm.includes(' ') || searchTerm.includes('-')) {
// 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;
// Merchant check
const merchantLink = article.querySelector('a[href*="merchant-id="]');
if (merchantLink) {
const merchantIDMatch = merchantLink.getAttribute('href').match(/merchant-id=(\d+)/);
if (merchantIDMatch && excludeMerchantIDs.includes(merchantIDMatch[1])) {
return true;
}
}
return false;
}
// ===== Deal-Management =====
// Deal ausblenden
function hideDeal(deal) {
deal.style.cssText = 'display: none !important';
deal.setAttribute('data-hidden-by-mydealz-manager', 'true');
}
// Letzten versteckten Deal speichern
function saveLastHiddenDeal() {
GM_setValue('lastHiddenDeal', lastHiddenDeal);
localStorage.setItem('lastHiddenDeal', JSON.stringify(lastHiddenDeal));
}
// Funktion zur Verarbeitung der Sortierung
function processSortRemembering() {
// Wenn Feature deaktiviert ist, lösche gespeicherte Sortierung
// und lass mydealz sein Standard-Verhalten nutzen
if (!window.rememberSort) {
// Prüfen, ob eine gespeicherte Sortierung existiert und entfernen
if (localStorage.getItem(PREFERRED_SORT_KEY)) {
localStorage.removeItem(PREFERRED_SORT_KEY);
if (DEBUG) console.log('[MDM Sort] Gespeicherte Sortierung gelöscht (Feature deaktiviert)');
}
return;
}
// Prüfen, ob wir auf einer Suchseite sind
if (window.location.pathname.includes('/search')) {
// Parameter aus URL auslesen
const params = new URLSearchParams(window.location.search);
const currentSort = params.get('sortBy');
// Wenn eine Sortierung vorhanden ist, speichern
if (currentSort) {
localStorage.setItem(PREFERRED_SORT_KEY, currentSort);
if (DEBUG) console.log('[MDM Sort] Sortierung gespeichert: ' + currentSort);
} else {
// Wenn keine Sortierung gesetzt ist, aber eine gespeicherte existiert
const savedSort = localStorage.getItem(PREFERRED_SORT_KEY);
if (savedSort && !window.location.href.includes('sortBy=')) {
// Neue URL mit der gespeicherten Sortierung erstellen
let newUrl = window.location.href;
const separator = newUrl.includes('?') ? '&' : '?';
newUrl += separator + 'sortBy=' + savedSort;
// Umleitung zur neuen URL mit kleiner Verzögerung
if (DEBUG) console.log('[MDM Sort] Wende Sortierung an: ' + savedSort);
setTimeout(() => {
window.location.href = newUrl;
}, 100);
}
}
}
// Suchformulare abfangen und anpassen
const searchForms = document.querySelectorAll('form[action*="/search"]');
searchForms.forEach(form => {
// Prüfen, ob das Formular bereits verarbeitet wurde
if (form.dataset.sortingModified === 'true') return;
// Markieren, dass das Formular verarbeitet wurde
form.dataset.sortingModified = 'true';
// Event-Listener für das Absenden des Formulars hinzufügen
form.addEventListener('submit', function(e) {
// Nur fortfahren, wenn Feature aktiviert ist
if (!window.rememberSort) return;
const savedSort = localStorage.getItem(PREFERRED_SORT_KEY);
if (savedSort) {
if (DEBUG) console.log('[MDM Sort] Füge Sortierung zur Suchanfrage hinzu: ' + savedSort);
// Prüfen, ob bereits ein sortBy-Feld vorhanden ist
let sortByInput = form.querySelector('input[name="sortBy"]');
// Wenn kein sortBy-Feld existiert, eines erstellen
if (!sortByInput) {
sortByInput = document.createElement('input');
sortByInput.type = 'hidden';
sortByInput.name = 'sortBy';
form.appendChild(sortByInput);
}
// Sortierung setzen
sortByInput.value = savedSort;
}
});
});
}
//#endregion
//#region --- 6. Initialisierung und Setup ---
function init() {
// ===== 1. Grundeinstellungen und gespeicherte Daten laden =====
// --- Grundeinstellungen ---
hideCustomBanners = GM_getValue('hideCustomBanners', false);
syncStorage();
excludeWords = loadExcludeWords();
loadSettings();
// --- Deal-Listen ---
hiddenDeals = GM_getValue('hiddenDeals', []);
recentHiddenDeals = GM_getValue('recentHiddenDeals', []);
// --- Letzter versteckter Deal ---
lastHiddenDeal = GM_getValue('lastHiddenDeal', null);
lastHiddenDealShown = GM_getValue(LAST_HIDDEN_DEAL_SHOWN, false);
// Automatische Markierung als "angezeigt" wenn kein Deal vorhanden
if (!lastHiddenDeal) {
lastHiddenDealShown = true;
GM_setValue(LAST_HIDDEN_DEAL_SHOWN, true);
}
// --- Sortierung merken ---
window.rememberSort = GM_getValue('rememberSort', true);
processSortRemembering();
// ===== 2. Filter-Einstellungen =====
// --- Preisfilter ---
maxPrice = parseFloat(localStorage.getItem(MAX_PRICE_KEY)) || 0;
// --- Kalte Deals ---
hideColdDeals = localStorage.getItem(HIDE_COLD_DEALS_KEY) === 'true';
// ===== 3. Feature-Flags und UI-Status =====
// --- Banner und Widgets ---
window.hideCustomBanners = hideCustomBanners;
updateCustomBannerIcon();
// --- Share Buttons ---
window.hideShareButtons = GM_getValue('hideShareButtons', false);
updateShareButtonsVisibility(window.hideShareButtons);
// --- Händlernamen ---
window.hideMatchingMerchantNames = GM_getValue('hideMatchingMerchantNames', false);
// ===== 5. System-Initialisierung =====
// --- UI ---
initializeUI();
initObserver();
}
//#endregion
//#region --- 7. Backup und Wiederherstellung ---
// ===== Backup-Funktionen =====
// --- Datei-Erstellung ---
function createBackupFile(backup, deviceName) {
const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
// Neues Datumsformat
const now = new Date();
const timestamp = now.toISOString()
.replace('T', '_') // T durch _ ersetzen
.split('.')[0] // Millisekunden entfernen
.replace(/:/g, '.') // : durch . ersetzen
.replace(/-/g, '-'); // - behalten
a.href = url;
a.download = `mydealz_backup_${deviceName}_${timestamp}.json`;
a.click();
URL.revokeObjectURL(url);
}
// --- Backup-Prozess ---
function backupData() {
try {
// 1. Daten laden
const currentWords = loadExcludeWords();
const currentMerchants = loadExcludeMerchants();
// 2. Backup-Objekt erstellen
const backup = {
excludeWords: currentWords,
merchantsData: currentMerchants,
maxPrice: maxPrice,
hideColdDeals: hideColdDeals
};
// 3. Geräte-Erkennung
let deviceType = "Desktop";
if (IS_TOUCH_DEVICE) {
// Überprüfe, ob es ein mobiles Gerät ist
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes('iphone') || userAgent.includes('ipad')) {
deviceType = "iOS";
} else if (userAgent.includes('android')) {
deviceType = "Android";
} else {
deviceType = "Tablet/Touch";
}
}
// 4. Gerätenamen-Verwaltung
const DEVICE_NAME_KEY = 'mdm_device_name';
const customDeviceName = localStorage.getItem(DEVICE_NAME_KEY) || GM_getValue(DEVICE_NAME_KEY, '');
// 5. Backup-Erstellung
if (!customDeviceName) {
// Neuen Gerätenamen abfragen
const newName = prompt(
"Wie möchtest du dieses Gerät nennen?\n" +
"Dies hilft dir, Backups von verschiedenen Geräten zu unterscheiden.",
deviceType
);
if (newName !== null) {
const deviceName = newName.trim() || deviceType;
// In beiden Speichern ablegen
localStorage.setItem(DEVICE_NAME_KEY, deviceName);
GM_setValue(DEVICE_NAME_KEY, deviceName);
// Mit dem neuen Namen das Backup erstellen
createBackupFile(backup, deviceName);
}
} else {
// Mit dem vorhandenen Namen das Backup erstellen
createBackupFile(backup, customDeviceName);
}
} catch (error) {
console.error('Backup error:', error);
alert('Fehler beim Erstellen des Backups: ' + error.message);
}
}
// ===== Wiederherstellungs-Funktionen =====
// --- Daten-Wiederherstellung ---
function restoreData(event) {
// 1. Datei-Validierung
const file = event.target.files[0];
if (!file || file.type !== 'application/json') {
console.error('Invalid file:', file);
alert('Bitte wählen Sie eine gültige JSON-Datei aus.');
return;
}
// 2. Datei-Verarbeitung
const reader = new FileReader();
reader.onload = function(e) {
try {
// 3. Daten-Parsing
const restoredData = JSON.parse(e.target.result);
// 4. Aktuelle Daten laden
const currentWords = new Set(loadExcludeWords());
const currentMerchants = new Map(
loadExcludeMerchants().map(m => [m.id, m])
);
// 5. Daten zusammenführen
// --- Wörter ---
restoredData.excludeWords.forEach(word => currentWords.add(word));
const mergedWords = Array.from(currentWords);
// --- Händler ---
restoredData.merchantsData.forEach(merchant => {
if (!currentMerchants.has(merchant.id)) {
currentMerchants.set(merchant.id, merchant);
}
});
const mergedMerchants = Array.from(currentMerchants.values());
// 6. Daten speichern
// --- Wortfilter ---
GM_setValue('excludeWords', mergedWords);
localStorage.setItem('excludeWords', JSON.stringify(mergedWords));
excludeWords = mergedWords;
// --- Händlerfilter ---
saveExcludeMerchants(mergedMerchants);
// --- Einstellungen ---
if (typeof restoredData.maxPrice === 'number' && maxPrice === 0) {
saveMaxPrice(restoredData.maxPrice);
}
if (typeof restoredData.hideColdDeals === 'boolean' && !hideColdDeals) {
hideColdDeals = restoredData.hideColdDeals;
GM_setValue('hideColdDeals', hideColdDeals);
localStorage.setItem('hideColdDeals', hideColdDeals);
}
// 7. UI aktualisieren
if (isSettingsOpen) {
updateUITheme();
}
processArticles();
alert('Backup wurde erfolgreich wiederhergestellt.');
} catch (error) {
console.error('Restore error:', error);
alert('Fehler beim Wiederherstellen des Backups: ' + error.message);
}
};
reader.readAsText(file);
}
//#endregion
//#region --- 8. Hilfsfunktionen ---
// ===== Text-Verarbeitung =====
// --- Händlernamen-Verarbeitung ---
function removeMerchantNameFromTitle(title, merchant) {
if (!title || !merchant) return title;
// Normalisiere Händlernamen und hole Konfiguration
const merchantName = merchant.toLowerCase()
.replace('...', '')
// 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: /(?<!\[.+)\s*(?:bei\s+)?netto\s+marken[-\s]discount[-\s]*/i, to: ' ' }
],
keepBrands: []
};
case 'Kaufland':
return {
abbreviations: [],
replacements: [
{ from: /kaufland[\s-]card/i, to: 'K-Card' },
// Entfernt "bei Kaufland" auch in der Mitte
{ from: /\s+bei\s+kaufland(?:\s*-\s*)/i, to: ' - ' }
],
keepBrands: ['Card']
};
case 'Amazon':
return {
abbreviations: [],
replacements: [
// Besonders behandelte Muster für Amazon Prime
{ from: /\[amazon\s+prime\]/i, to: '[Prime]' },
{ from: /\[amazon\s+(prime\s+\w+)\]/i, to: '[$1]' },
// Ersetze "Amazon Prime" mit "Prime" (außerhalb von Klammern)
{ from: /\bamazon\s+prime\b/i, to: 'Prime' },
// Erhalte "Prime Video", "Prime Gaming" usw.
{ from: /\bamazon\s+(prime\s+\w+)\b/i, to: '$1' }
],
keepBrands: ['Prime']
};
case 'Best Secret':
return {
abbreviations: ['BestSecret'],
replacements: [],
keepBrands: []
};
case 'TradePub.com': // Füge zusätzliche Schreibweise hinzu
return {
abbreviations: ['tradepub', 'Tradepub'], // Beide Schreibweisen
replacements: [
{ from: /\s+bei\s+tradepub\.com(?:\s+|$)/i, to: ' ' },
{ from: /^tradepub\.com:\s*/i, to: '' },
{ from: /^\(tradepub\)\s*/i, to: '' },
// Neues Pattern für (Tradepub) mit Groß-/Kleinschreibung
{ from: /^\((?:Tradepub|TRADEPUB|tradepub)\)\s*/i, to: '' }
],
keepBrands: []
};
case 'Netto':
return {
abbreviations: ['netto mit hund'],
replacements: [
// Entfernt "[Netto mit Hund]" Format
{ from: /\[netto\s+mit\s+hund\]\s*/i, to: '' }
],
keepBrands: []
};
case 'A.T.U':
return {
abbreviations: ['atu', 'a.t.u'],
replacements: [
{ from: /(Sale)\s+bei\s+(?:ATU|A\.T\.U)(\s*;\s*)(Ab)/i, to: '$1$2$3' }
],
keepBrands: []
};
case 'Uber Eats':
return {
abbreviations: ['ubereats'],
replacements: [
// Spezifisches Pattern für "[UberEats Member Days]" Format
{ from: /\[UberEats\s+(Member\s+Days)\]/i, to: '[$1]' }
],
keepBrands: ['Member Days']
};
default:
return { abbreviations: [], replacements: [], keepBrands: [] };
}
};
const config = getMerchantConfig(merchant);
// Führe erst spezielle Ersetzungen durch
let result = title;
config.replacements.forEach(({ from, to }) => {
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;
// Der wichtige Teil - wenn das UI geschlossen wird und ein Deal ausgeblendet wurde
if (lastHiddenDeal && !lastHiddenDealShown) {
lastHiddenDealShown = true;
GM_setValue(LAST_HIDDEN_DEAL_SHOWN, true);
}
// Add word suggestion list cleanup
const suggestionList = document.getElementById('wordSuggestionList');
if (suggestionList) {
suggestionList.remove();
}
// Close merchant & words lists with state logging
if (merchantListDiv?.parentNode) {
merchantListDiv.remove();
}
if (wordsListDiv?.parentNode) {
wordsListDiv.remove();
}
// Schließe auch das Sidebar Elements UI wenn vorhanden
const sidebarElementsDiv = document.getElementById('sidebarElementsDiv');
if (sidebarElementsDiv?.parentNode) {
sidebarElementsDiv.remove();
}
// Reset UI states with logging
if (activeSubUI) {
const btn = document.getElementById(`show${activeSubUI === 'merchant' ? 'Merchant' : 'Words'}ListButton`);
if (btn) {
btn.innerHTML = activeSubUI === 'merchant' ?
'<i class="fas fa-store"></i> Händlerfilter verwalten' :
'<i class="fas fa-list"></i> Wortfilter verwalten';
btn.removeAttribute('data-processing');
}
}
activeSubUI = null;
dealThatOpenedSettings = null; // Reset auch den aktiven Deal
// Clean up handlers
document.removeEventListener('click', suggestionClickHandler);
document.removeEventListener('click', uiClickOutsideHandler);
window.removeEventListener('unload', cleanup);
uiClickOutsideHandler = null;
// Reset suggestion state
suggestedWords = [];
}
//#endregion
//#region --- 9. Theming und UI-Darstellung ---
// Farbkonstanten für Light/Dark Mode
const THEME_COLORS = {
light: {
background: '#ffffff',
border: 'rgba(3,12,25,0.23)',
text: '#333333',
buttonBg: '#f5f5f5',
buttonBorder: '#d0d0d0',
inputBg: '#ffffff',
itemBg: '#f8f8f8'
},
dark: {
background: '#1d1f20',
border: 'rgb(107, 109, 109)',
text: '#ffffff',
buttonBg: '#2d2d2d',
buttonBorder: '#3d3d3d',
inputBg: '#1d1f20',
itemBg: '#2a2a2a'
}
};
// Theme Observer erstellen
const themeObserver = new MutationObserver(() => {
requestAnimationFrame(() => {
const colors = getThemeColors();
updateAllUIThemes(colors);
});
});
// Observer für beide Elemente einrichten
const targetNodes = [document.documentElement, document.body];
targetNodes.forEach(node => {
themeObserver.observe(node, {
attributes: true,
attributeFilter: ['class', 'data-theme']
});
});
// System Theme Observer
const systemThemeObserver = window.matchMedia('(prefers-color-scheme: dark)');
systemThemeObserver.addEventListener('change', () => {
requestAnimationFrame(() => {
const colors = getThemeColors();
updateAllUIThemes(colors);
});
});
// Hide Button Theme Observer
const hideButtonThemeObserver = new MutationObserver(() => {
const colors = getThemeColors();
requestAnimationFrame(() => {
document.querySelectorAll('.custom-hide-button').forEach(button => {
if (button) {
button.style.cssText = `
position: absolute !important;
left: 50% !important;
top: 50% !important;
transform: translate(-50%, -50%) !important;
z-index: 10002 !important;
background: ${colors.background} !important;
border: 1px solid ${colors.border} !important;
border-radius: 50% !important;
cursor: pointer !important;
padding: 4px !important;
width: 28px !important;
height: 28px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
pointer-events: all !important;
box-shadow: none !important;
font-size: 12px !important;
`;
}
});
// Update settings UI wenn offen
if (isSettingsOpen) {
updateUITheme();
}
});
});
// Start observing theme changes
hideButtonThemeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
// Theme Update Funktionen
function updateAllUIThemes() {
const colors = getThemeColors();
// Update buttons
document.querySelectorAll('.custom-hide-button').forEach(button => {
if (button) {
button.style.setProperty('background', colors.background, 'important');
button.style.setProperty('border-color', colors.border, 'important');
}
});
// Update open UIs
if (isSettingsOpen || activeSubUI) {
updateUITheme();
}
}
//#endregion
//#region --- 10. Button-Management ---
function addSettingsButton() {
const deals = document.querySelectorAll('article.thread--deal, article.thread--voucher');
deals.forEach(deal => {
if (deal.hasAttribute('data-settings-added')) return;
const footer = deal.querySelector('.threadListCard-footer, .threadCardLayout-footer');
if (!footer) return;
// Create settings button
const settingsBtn = document.createElement('button');
settingsBtn.className = 'flex--shrink-0 button button--type-text button--mode-secondary button--square';
settingsBtn.title = 'mydealz Manager Einstellungen';
settingsBtn.setAttribute('data-t', 'mdmSettings');
settingsBtn.style.cssText = `
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
padding: 6px !important;
border: none !important;
background: transparent !important;
cursor: pointer !important;
margin: 0 4px !important;
min-width: 32px !important;
min-height: 32px !important;
position: relative !important;
z-index: 2 !important;
`;
settingsBtn.innerHTML = `
<span class="flex--inline boxAlign-ai--all-c">
<svg width="20" height="20" class="icon icon--gear">
<use xlink:href="/assets/img/ico_707ed.svg#gear"></use>
</svg>
</span>
`;
// Insert at correct position (before comments button)
const commentsBtn = footer.querySelector('[href*="comments"]');
if (commentsBtn) {
commentsBtn.parentNode.insertBefore(settingsBtn, commentsBtn);
} else {
footer.prepend(settingsBtn);
}
deal.setAttribute('data-settings-added', 'true');
settingsBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
if (isSettingsOpen) {
if (dealThatOpenedSettings === deal) {
cleanup();
} else {
// Komplett neues UI erstellen statt nur den Button zu aktualisieren
cleanup();
dealThatOpenedSettings = deal;
createSettingsUI(); // Dies erstellt das UI in der korrekten Reihenfolge
}
} else {
dealThatOpenedSettings = deal;
createSettingsUI();
}
return false;
};
});
}
function addHideButtons() {
const deals = document.querySelectorAll('article:not([data-button-added])');
deals.forEach(deal => {
if (deal.hasAttribute('data-button-added')) return;
// Check for expired status
const isExpired = deal.querySelector('.color--text-TranslucentSecondary .size--all-s')?.textContent.includes('Abgelaufen');
// Get temperature container
const voteTemp = deal.querySelector('.cept-vote-temp');
if (!voteTemp) return;
// Remove popover
const popover = voteTemp.querySelector('.popover-origin');
if (popover) popover.remove();
// Find temperature span for expired deals
const tempSpan = isExpired ? voteTemp.querySelector('span') : null;
const targetElement = isExpired ? tempSpan : voteTemp;
if (!targetElement) return;
const hideButtonContainer = document.createElement('div');
hideButtonContainer.style.cssText = `
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: none;
z-index: 10001;
pointer-events: none;
`;
const hideButton = document.createElement('button');
hideButton.innerHTML = '❌';
hideButton.className = 'vote-button overflow--visible custom-hide-button';
hideButton.title = 'Deal verbergen';
hideButton.style.cssText = `
position: absolute !important;
left: 50% !important;
top: 50% !important;
transform: translate(-50%, -50%) !important;
z-index: 10002 !important;
background: ${getThemeColors().background} !important;
border: 1px solid ${getThemeColors().border} !important;
border-radius: 50% !important;
cursor: pointer !important;
padding: 4px !important;
width: 28px !important;
height: 28px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
pointer-events: all !important;
box-shadow: none !important;
font-size: 12px !important;
`;
// Position relative to container
if (!targetElement.style.position) {
targetElement.style.position = 'relative';
}
if (IS_TOUCH_DEVICE) {
let buttonVisible = false;
const dealId = deal.getAttribute('id');
// Add scroll handler to hide button
const scrollHandler = () => {
if (buttonVisible) {
buttonVisible = false;
hideButtonContainer.style.display = 'none';
} else if (hideButtonContainer.style.display === 'block') {
}
};
// Add scroll listener
window.addEventListener('scroll', scrollHandler, { passive: true });
targetElement.addEventListener('touchstart', (e) => {
e.preventDefault();
e.stopPropagation();
if (!buttonVisible) {
buttonVisible = true;
hideButtonContainer.style.display = 'block';
} else {
const dealId = deal.getAttribute('id');
const dealTitle = deal.querySelector('.thread-title')?.textContent.trim() || 'Unbekannter Deal';
// Aktuellen Deal speichern
lastHiddenDeal = {
id: dealId,
title: dealTitle,
timestamp: Date.now()
};
// "Deal wurde angezeigt" auf false setzen
lastHiddenDealShown = false;
GM_setValue(LAST_HIDDEN_DEAL_SHOWN, false);
// Speichern des letzten ausgeblendeten Deals
saveLastHiddenDeal();
hiddenDeals.push(dealId);
saveHiddenDeals();
hideDeal(deal);
window.removeEventListener('scroll', scrollHandler);
}
}, true);
targetElement.addEventListener('touchend', () => {
if (!buttonVisible) {
hideButtonContainer.style.display = 'none';
}
}, true);
} else {
targetElement.addEventListener('mouseenter', () => {
hideButtonContainer.style.display = 'block';
}, true);
targetElement.addEventListener('mouseleave', () => {
hideButtonContainer.style.display = 'none';
}, true);
hideButton.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const dealId = deal.getAttribute('id');
const dealTitle = deal.querySelector('.thread-title')?.textContent.trim() || 'Unbekannter Deal';
// Aktuellen Deal speichern
lastHiddenDeal = {
id: dealId,
title: dealTitle,
timestamp: Date.now()
};
// "Deal wurde angezeigt" auf false setzen und persistieren
lastHiddenDealShown = false;
GM_setValue(LAST_HIDDEN_DEAL_SHOWN, false);
saveLastHiddenDeal();
hiddenDeals.push(dealId);
saveHiddenDeals();
hideDeal(deal);
return false;
};
}
hideButtonContainer.appendChild(hideButton);
targetElement.appendChild(hideButtonContainer);
deal.setAttribute('data-button-added', 'true');
});
}
function getMerchantButtonText(merchantName) {
return `Alle Deals von ${merchantName}`;
}
function addMerchantPageHideButton() {
const urlParams = new URLSearchParams(window.location.search);
const merchantId = urlParams.get('merchant-id');
const merchantBanner = document.querySelector(MERCHANT_PAGE_SELECTOR);
const merchantName = document.querySelector('.merchant-banner__title')?.textContent.trim();
if (!merchantId || !merchantBanner || !merchantName) return;
const hideButtonContainer = document.createElement('div');
hideButtonContainer.style.cssText = `
display: inline-flex;
align-items: center;
margin-left: 10px;
`;
const hideButton = document.createElement('button');
hideButton.style.cssText = `
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
`;
const buttonText = getMerchantButtonText(merchantName);
hideButton.innerHTML = `
<span style="display: inline-flex; align-items: center; gap: 8px; white-space: nowrap;">
<i class="fas fa-store-slash"></i>
<span>${buttonText}</span>
</span>
`;
hideButton.title = `${buttonText} ausblenden`;
hideButton.addEventListener('click', () => {
const merchantsData = loadExcludeMerchants();
if (!merchantsData.some(m => m.id === merchantId)) {
merchantsData.unshift({ id: merchantId, name: merchantName });
saveExcludeMerchants(merchantsData);
processArticles();
}
});
hideButtonContainer.appendChild(hideButton);
merchantBanner.appendChild(hideButtonContainer);
}
// Helfer-Funktion, um den Zustand der Toggle-Buttons zu aktualisieren
function updateToggleButtonsState() {
const buttons = document.querySelectorAll('.toggle-sidebar-element');
buttons.forEach(button => {
const key = button.dataset.key;
if (key && SIDEBAR_ELEMENTS[key]) {
const element = SIDEBAR_ELEMENTS[key];
const icon = button.querySelector('i');
if (icon) {
icon.className = `fas ${element.hidden ? 'fa-eye-slash' : 'fa-eye'}`;
icon.setAttribute('aria-label', element.hidden ? 'Element versteckt' : 'Element sichtbar');
}
}
});
}
//#endregion
//#region --- 11. Skript-Initialisierung ---
// Initial beim Start aufrufen
registerMenuCommands();
// Start script - nach DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Aufräumen bestehender Daten beim Skriptstart
(function cleanupMerchantData() {
const merchants = loadExcludeMerchants();
saveExcludeMerchants(merchants);
})();
//#endregion
//#region --- 12. Spezialisierte Komponenten ---
function initializeUI() {
// Initial UI Setup
processArticles();
addSettingsButton();
addHideButtons();
addMerchantPageHideButton();
}
// Observer Initialisierung
function initObserver() {
observer.disconnect();
observer.observe(document.body, {
childList: true,
subtree: true
});
// Sofortige Verarbeitung
requestAnimationFrame(() => {
processArticles();
addSettingsButton();
addHideButtons();
});
}
function registerMenuCommands() {
// Alte Menüs erst abmelden
if (menuCommandId !== undefined) {
GM_unregisterMenuCommand(menuCommandId);
}
// Menüeintrag zum Öffnen der Einstellungen registrieren
menuCommandId = GM_registerMenuCommand('mydealz Manager Einstellungen', () => {
// Einstellungen für aktuellen Deal öffnen
dealThatOpenedSettings = document.querySelector('article.thread--deal, article.thread--voucher');
createSettingsUI();
});
}
//#endregion
//#region --- 13. Dokumentation ---
/*
===================================================================================
--- Funktionsübersicht mydealz Manager ---
===================================================================================
detectMultipleInstances() - Erkennt parallele Ausführungen des Scripts (ab 1.13.x)
getThemeColors() - Liefert Theme-spezifische Farben basierend auf aktuellem Theme
processArticles() - Verarbeitet und filtert alle Deals
shouldExcludeArticle() - Prüft ob ein Deal ausgeblendet werden soll
createSettingsUI() - Erstellt das Haupteinstellungsfenster
addSettingsButton() - Fügt Einstellungs-Button zu Deals hinzu
addHideButtons() - Fügt X-Button zum Ausblenden hinzu
backupData() - Erstellt Backup der Einstellungen
restoreData() - Stellt Backup-Daten wieder her
decodeHtml() - Konvertiert HTML-Entities
cleanup() - Räumt UI-Elemente auf
syncStorage() - Synchronisiert GM und localStorage
saveExcludeWords() - Speichert Wortfilter
loadExcludeWords() - Lädt Wortfilter
saveExcludeMerchants() - Speichert Händlerfilter
loadExcludeMerchants() - Lädt Händlerfilter
saveMaxPrice() - Speichert Maximalpreis
createMerchantListUI() - Zeigt Händlerliste
createExcludeWordsUI() - Zeigt Wortfilterliste
updateActiveLists() - Aktualisiert Listen im UI
handleMerchantDelete() - Löscht Händler aus Filter
handleWordDelete() - Löscht Wort aus Filter
setupScrollHandling() - Konfiguriert Scroll-Verhalten
updateUITheme() - Aktualisiert UI-Farben
init() - Initialisiert das Script
removeMerchantNameFromTitle() - Entfernt Händlernamen aus Deal-Titeln
throttle() - Begrenzt die Ausführungshäufigkeit von Funktionen
getWordsFromTitle() - Extrahiert relevante Wörter aus Deal-Titeln
hideDeal() - Blendet einen Deal aus
initUIContainers() - Initialisiert UI-Container
updateSuggestionList() - Aktualisiert Wortvorschläge
handleWordSelection() - Verarbeitet Wortauswahl
setupClickOutsideHandler() - Konfiguriert Außenbereich-Klicks
createSuggestionClickHandler() - Erstellt Handler für Wortvorschläge
registerMenuCommands() - Registriert Script-Manager Menüeinträge
saveHiddenDeals() - Speichert ausgeblendete Deals
saveLastHiddenDeal() - Speichert zuletzt ausgeblendeten Deal
initializeUI() - Initialisiert Benutzeroberfläche
initObserver() - Initialisiert DOM-Beobachter
addMerchantPageHideButton() - Fügt Händler-Ausblenden-Button hinzu
injectDealFilters() - Fügt Deal-Filter in die UI ein
===================================================================================
*/
//#endregion