Greasy Fork

Händlerlogo im Deal

Fügt das Logo des Händlers als Hintergrund in jeden passenden Deal. Entfernt den Namen des Händlers aus dem Titel.

当前为 2025-01-03 提交的版本,查看 最新版本

// ==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();
    }
})();