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