您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Fügt das Logo des Händlers als Hintergrund in jeden passenden Deal. Entfernt den Namen des Händlers aus dem Titel.
当前为
// ==UserScript== // @name Händlerlogo im Deal // @namespace http://tampermonkey.net/ // @version 1.0 // @description Fügt das Logo des Händlers als Hintergrund in jeden passenden Deal. Entfernt den Namen des Händlers aus dem Titel. // @author Flo (https://github.com/9jS2PL5T) (https://www.mydealz.de/profile/Basics0119) // @license MIT // @match https://www.mydealz.de/* // @icon https://www.google.com/s2/favicons?sz=64&domain=mydealz.de // @grant none // ==/UserScript== // Add before IIFE const LOGO_ENABLED_KEY = 'merchantLogoEnabled'; const TITLE_CLEAN_KEY = 'merchantTitleClean'; const LOGO_CACHE_KEY_PREFIX = 'logo_cache_'; const CACHE_EXPIRY = 7 * 24 * 60 * 60 * 1000; // 1 week in ms (function() { 'use strict'; // Add toggle states let showLogos = localStorage.getItem(LOGO_ENABLED_KEY) !== 'false'; let cleanTitles = localStorage.getItem(TITLE_CLEAN_KEY) !== 'false'; // Create toggle buttons const createToggleButtons = () => { const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` position: fixed; bottom: 20px; right: 20px; z-index: 9999; display: flex; gap: 10px; `; const logoButton = document.createElement('button'); const titleButton = document.createElement('button'); const buttonStyle = ` padding: 8px 16px; border-radius: 4px; border: none; cursor: pointer; background: #2c7; color: white; font-weight: bold; `; [logoButton, titleButton].forEach(btn => { btn.style.cssText = buttonStyle; }); const updateButtons = () => { logoButton.textContent = `Logos: ${showLogos ? 'An' : 'Aus'}`; logoButton.style.background = showLogos ? '#2c7' : '#777'; titleButton.textContent = `Händler: ${cleanTitles ? 'Aus' : 'An'}`; titleButton.style.background = cleanTitles ? '#2c7' : '#777'; }; logoButton.onclick = () => { showLogos = !showLogos; localStorage.setItem(LOGO_ENABLED_KEY, showLogos); updateButtons(); // Live update logos document.querySelectorAll('.merchant-logo-bg').forEach(logo => { logo.style.display = showLogos ? 'block' : 'none'; }); // Process unprocessed deals if enabled if (showLogos) { processDeals(); } }; titleButton.onclick = () => { cleanTitles = !cleanTitles; localStorage.setItem(TITLE_CLEAN_KEY, cleanTitles); updateButtons(); document.querySelectorAll([ 'article.thread--deal', 'article.thread--voucher' ].join(', ')).forEach(deal => { const merchantLink = deal.querySelector('a[data-t="merchantLink"]'); const titleLink = deal.querySelector('.thread-title .thread-link'); if (merchantLink && titleLink) { const merchantName = merchantLink.textContent.trim(); console.debug('Processing:', { merchantName, currentTitle: titleLink.textContent, cleanTitles }); // Store original title if not already stored if (!deal.hasAttribute('data-original-title')) { deal.setAttribute('data-original-title', titleLink.textContent); console.debug('Storing original:', titleLink.textContent); } const originalTitle = deal.getAttribute('data-original-title'); if (originalTitle) { if (cleanTitles) { // Enhanced pattern to handle all cases const newTitle = cleanMerchantFromTitle(originalTitle, merchantName); console.debug('Cleaned to:', newTitle); titleLink.textContent = newTitle; } else { console.debug('Restoring to:', originalTitle); titleLink.textContent = originalTitle; } } } }); }; updateButtons(); buttonContainer.appendChild(logoButton); buttonContainer.appendChild(titleButton); document.body.appendChild(buttonContainer); }; // Add initialization function const initializeDeals = () => { document.querySelectorAll([ 'article.thread--deal', 'article.thread--voucher' ].join(', ')).forEach(deal => { const merchantLink = deal.querySelector('a[data-t="merchantLink"]'); const titleLink = deal.querySelector('.thread-title .thread-link'); if (merchantLink && titleLink) { const merchantName = merchantLink.textContent.trim(); // Store original title if not already stored if (!deal.hasAttribute('data-original-title')) { deal.setAttribute('data-original-title', titleLink.textContent); } const originalTitle = deal.getAttribute('data-original-title'); if (originalTitle && cleanTitles) { // Enhanced pattern to handle all cases const newTitle = cleanMerchantFromTitle(originalTitle, merchantName); titleLink.textContent = newTitle; } } }); }; // Constants const PROCESSED_ATTR = 'data-bg-processed'; // Extract merchant ID from deal const getMerchantId = (dealElement) => { try { // Find merchant link with all possible patterns const merchantLink = dealElement.querySelector([ 'a[href*="deals?merchant-id="]', 'a[href*="gutscheine?merchant-id="]', 'a[href*="search/gutscheine?merchant-id="]' ].join(', ')); if (!merchantLink) return null; // Extract ID from URL pattern const match = merchantLink.href.match(/merchant-id=(\d+)/); return match ? match[1] : null; } catch (error) { console.debug('[Merchant BG] Error extracting merchant ID:', error); return null; } }; // Build logo URL for merchant const getLogoUrl = (merchantId, version = 1) => { return `https://static.mydealz.de/merchants/raw/avatar/${merchantId}_${version}/re/140x140/qt/70/${merchantId}_${version}.jpg`; }; const checkLogoExists = async (url) => { try { const response = await fetch(url, { method: 'HEAD' }); return response.ok; } catch (error) { return false; } }; // Add cache management const logoCache = { set: (merchantId, data) => { try { localStorage.setItem(`${LOGO_CACHE_KEY_PREFIX}${merchantId}`, JSON.stringify({ ...data, timestamp: Date.now() })); } catch (e) { console.debug('[Logo Cache] Storage failed:', e); } }, get: (merchantId) => { try { const data = localStorage.getItem(`${LOGO_CACHE_KEY_PREFIX}${merchantId}`); if (!data) return null; const parsed = JSON.parse(data); // Expire after 1 week if (Date.now() - parsed.timestamp > CACHE_EXPIRY) { localStorage.removeItem(`${LOGO_CACHE_KEY_PREFIX}${merchantId}`); return null; } return parsed; } catch (e) { console.debug('[Logo Cache] Read failed:', e); return null; } }, clear: () => { try { Object.keys(localStorage) .filter(key => key.startsWith(LOGO_CACHE_KEY_PREFIX)) .forEach(key => localStorage.removeItem(key)); } catch (e) { console.debug('[Logo Cache] Clear failed:', e); } } }; // Throttle requests const queue = []; // Removed TypeScript type let processing = false; const findLatestLogoVersion = async function(merchantId) { // Removed TypeScript types try { let highestVersion = null; // Try versions 1-3 for (let version = 1; version <= 3; version++) { const url = getLogoUrl(merchantId, version); const exists = await checkLogoExists(url); if (exists) { highestVersion = version; } } return highestVersion; } catch (error) { console.error(`Error checking logo versions for merchant ${merchantId}:`, error); return null; } }; const processQueue = async () => { if (processing || queue.length === 0) return; processing = true; try { const deal = queue.shift(); if (!deal) return; const merchantId = getMerchantId(deal); if (!merchantId) return; // Check cache first const cached = logoCache.get(merchantId); if (cached?.url) { console.debug(`[Logo Cache] Hit for merchant ${merchantId}`); applyLogo(deal, cached.url); return; } // Find latest version if not cached const version = await findLatestLogoVersion(merchantId); if (!version) return; const logoUrl = getLogoUrl(merchantId, version); // Cache result but don't track processed merchants logoCache.set(merchantId, { url: logoUrl, version }); // Apply logo console.debug(`[Logo] Found for merchant ${merchantId} at version ${version}`); applyLogo(deal, logoUrl); } catch (error) { console.error('Error processing queue:', error); } finally { processing = false; if (queue.length > 0) { void processQueue(); } } }; const applyLogo = (deal, logoUrl) => { if (!logoUrl || !deal || deal.querySelector('.merchant-logo-bg')) return; // Set deal content styles deal.style.position = 'relative'; deal.style.overflow = 'hidden'; // Create and style logo container const container = document.createElement('div'); container.classList.add('merchant-logo-bg'); // Add class for tracking container.style.cssText = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 200px; height: 200px; background-image: url("${logoUrl}"); background-size: contain; background-position: center; background-repeat: no-repeat; border-radius: 50%; opacity: 0.25; pointer-events: none; z-index: 0; `; deal.insertBefore(container, deal.firstChild); deal.setAttribute(PROCESSED_ATTR, 'true'); }; // Add type safety const shouldCleanTitle = (title, merchantName) => { // Include partial matches within brackets/parentheses const patterns = [ // Full merchant name new RegExp(`\\s*\\(${merchantName}\\)\\s*$`, 'i'), new RegExp(`^\\(${merchantName}\\)\\s*`, 'i'), new RegExp(`\\s*\\[${merchantName}\\]\\s*$`, 'i'), new RegExp(`^\\[${merchantName}\\]\\s*`, 'i'), // Merchant name with additional content in brackets new RegExp(`\\[(.*\\s)?${merchantName}(\\s.*)?\\]`, 'i'), new RegExp(`\\((.*\\s)?${merchantName}(\\s.*)?\\)`, 'i'), // Plain merchant name new RegExp(`\\s*[-–|]?\\s*${merchantName}\\s*$`, 'i'), new RegExp(`^${merchantName}\\s*[-–|]?\\s*`, 'i') ]; return patterns.some(pattern => pattern.test(title)); }; const cleanMerchantFromTitle = (title, merchantName) => { let cleanTitle = title; // Check for merchant name alone in brackets/parentheses const soloPatterns = [ new RegExp(`^\\[${merchantName}\\]\\s*`, 'i'), new RegExp(`^\\(${merchantName}\\)\\s*`, 'i') ]; // If merchant name is alone in brackets, remove entire bracket section if (soloPatterns.some(pattern => pattern.test(title))) { cleanTitle = cleanTitle .replace(new RegExp(`^\\[${merchantName}\\]\\s*`, 'i'), '') .replace(new RegExp(`^\\(${merchantName}\\)\\s*`, 'i'), ''); } else { // Otherwise handle mixed content in brackets cleanTitle = cleanTitle // Handle merchant name within brackets .replace(new RegExp(`\\[(.*\\s)?${merchantName}(\\s+)(.+?)\\]`, 'i'), '[$3]') .replace(new RegExp(`\\[${merchantName}(\\s+)(.+?)\\]`, 'i'), '[$2]') .replace(new RegExp(`\\[(.+?)(\\s+)${merchantName}\\]`, 'i'), '[$1]') // Handle merchant name within parentheses .replace(new RegExp(`\\((.*\\s)?${merchantName}(\\s+)(.+?)\\)`, 'i'), '($3)') .replace(new RegExp(`\\(${merchantName}(\\s+)(.+?)\\)`, 'i'), '($2)') .replace(new RegExp(`\\((.+?)(\\s+)${merchantName}\\)`, 'i'), '($1)') // Handle standalone merchant name .replace(new RegExp(`\\s*[-–|]?\\s*${merchantName}\\s*$`, 'i'), '') .replace(new RegExp(`^${merchantName}\\s*[-–|]?\\s*`, 'i'), ''); } return cleanTitle .replace(/^\s*[:]\s*/, '') .replace(/\s+/g, ' ') .trim(); }; const processDeal = (deal) => { if (!deal || deal.hasAttribute(PROCESSED_ATTR)) return; if (cleanTitles) { const merchantLink = deal.querySelector('a[data-t="merchantLink"]'); const titleLink = deal.querySelector('.thread-title .thread-link'); if (merchantLink && titleLink) { const merchantName = merchantLink.textContent.trim(); const title = titleLink.textContent; // Only store if title contains merchant name if (shouldCleanTitle(title, merchantName) && !deal.hasAttribute('data-original-title')) { deal.setAttribute('data-original-title', title); console.debug('Storing original:', title); } if (deal.hasAttribute('data-original-title')) { const cleanTitle = cleanMerchantFromTitle(title, merchantName); titleLink.textContent = cleanTitle; } } } if (showLogos) { queue.push(deal); void processQueue(); } }; // Debounce observer callback const debounce = (fn, delay) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; }; // Create debounced processDeals const debouncedProcessDeals = debounce(() => { document.querySelectorAll([ 'article.thread--deal:not([data-bg-processed])', 'article.thread--voucher:not([data-bg-processed])' ].join(', ')) .forEach(element => { if (element instanceof HTMLElement && !element.querySelector('.merchant-logo-bg')) { processDeal(element); } }); }, 250); // Update observer const observer = new MutationObserver(debouncedProcessDeals); // Add error handling const processDeals = () => { try { document.querySelectorAll([ 'article.thread--deal:not([data-bg-processed])', 'article.thread--voucher:not([data-bg-processed])' ].join(', ')) .forEach((element) => { if (element instanceof HTMLElement) { processDeal(element); } }); } catch (error) { console.error('Error processing deals:', error); } }; // Add cleanup // Start observing with disconnect handling try { createToggleButtons(); initializeDeals(); observer.observe(document.body, { childList: true, subtree: true }); // Initial processing processDeals(); } catch (error) { console.error('Error setting up observer:', error); observer.disconnect(); } })();