Cookidoo Filter

Filtert Rezepte nach Anzahl der Bewertungen + färbt Rezepte nach Bewertungen

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Cookidoo Filter 
// @namespace    https://cookidoo.de/
// @version      1.0
// @description  Filtert Rezepte nach Anzahl der Bewertungen + färbt Rezepte nach Bewertungen
// @author       Uli König
// @match        https://cookidoo.de/*
// @run-at       document-idle
// @grant        none
// @license      GPL-3.0-or-later
// ==/UserScript==

(function() {
    'use strict';

    let MIN_RATINGS = 100;

    function parseCount(text) {
        if (!text || typeof text !== 'string') return 0;
        let t = text.trim();

        const kMatch = t.match(/(\d+(?:[.,]\d+)?)\s*k/i);
        if (kMatch) {
            let numPart = kMatch[1].replace(',', '.');
            let val = parseFloat(numPart);
            if (!isNaN(val)) return Math.round(val * 1000);
        }

        if (/[.,]/.test(t)) {
            let val = parseFloat(t.replace(',', '.'));
            if (!isNaN(val) && val <= 5.0) return 0;
        }

        let cleanInt = t.replace(/[^\d]/g, '');
        let valInt = parseInt(cleanInt, 10);
        return isNaN(valInt) ? 0 : valInt;
    }

    function parseAverage(text) {
        if (!text) return 0;
        // Erweiterte Regex für Noten (4.6, 4,6, 4.0 etc.)
        const avgMatch = text.match(/(\d[.,]?\d?)/);
        if (avgMatch) {
            let val = parseFloat(avgMatch[1].replace(',', '.'));
            if (val >= 1.0 && val <= 5.0) return val;
        }
        return 0;
    }

    function getColorForRating(rating) {
        if (rating === 0) return '';
        const r1 = 255, g1 = 182, b1 = 193;
        const r2 = 255, g2 = 242, b2 = 204;
        const r3 = 200, g3 = 230, b3 = 201;

        let t;
        if (rating <= 3.0) {
            t = (rating - 1.0) / 2.0;
            return `rgb(${Math.round(r1)}, ${Math.round(g1 + t * (g2 - g1))}, ${Math.round(b1 + t * (b2 - b1))})`;
        } else {
            t = (rating - 3.0) / 2.0;
            return `rgb(${Math.round(r2 - t * (r2 - r3))}, ${Math.round(g2 + t * (g3 - g2))}, ${Math.round(b2 + t * (b3 - b2))})`;
        }
    }

    function processTiles(root) {
        if (!root || root.nodeType !== 1) return;

        // **ERWEITERTE Tile-Suche für alle Seiten**
        const selectors = [
            'core-tile.core-tile--expanded',
            'core-tile',
            '[class*="recipe-tile"]',
            '[class*="tile"]',
            '[class*="card"]',           // Sammlungen
            '[data-testid*="recipe"]',   // Test-IDs
            '.recipe-item',              // Fallback
            '.item'                      // Generisch
        ];

        let tiles = [];
        selectors.forEach(selector => {
            const found = root.querySelectorAll(selector);
            tiles = tiles.concat(Array.from(found));
        });

        // Entferne Duplikate
        tiles = [...new Set(tiles)];

        if (tiles.length === 0) {
            console.log('[Filter] Keine Tiles gefunden');
            return;
        }

        console.log(`[Filter] Prüfe ${tiles.length} Tiles...`);

        tiles.forEach((tile, index) => {
            let maxCount = 0;
            let avgRating = 0;

            // Alle Text-Elemente im Tile durchsuchen
            const allElements = tile.querySelectorAll('*');
            allElements.forEach(el => {
                if (el.children.length === 0 && el.textContent && el.textContent.trim().length > 0) {
                    const txt = el.textContent.trim();
                    if (/\d/.test(txt)) {
                        const count = parseCount(txt);
                        if (count > maxCount) maxCount = count;

                        const avg = parseAverage(txt);
                        if (avg > avgRating) avgRating = avg;
                    }
                }
            });

            // Filter
            if (maxCount > 0 && maxCount < MIN_RATINGS) {
                tile.setAttribute('hidden', 'true');
            } else {
                tile.removeAttribute('hidden');
                // **FARBUNG (verbessert)**
                if (avgRating > 0) {
                    const color = getColorForRating(avgRating);
                    if (color) {
                        // Verschiedene Styling für verschiedene Kacheln
                        tile.style.backgroundColor = color;
                        tile.style.borderRadius = '8px';
                        tile.style.transition = 'background-color 0.3s ease';

                        // Sanfter Rand
                        tile.style.border = '1px solid rgba(255,255,255,0.3)';

                        // Hover-Effekt
                        tile.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
                    }
                }
            }
        });
    }

    function addFilterInput() {
        const oldInput = document.getElementById('tm-filter-input-group');
        if (oldInput) oldInput.remove();

        // 1. filterButton (Suche)
        const filterButton = document.getElementById('filterButton');
        if (filterButton) {
            const inputGroup = createInputGroup();
            filterButton.parentNode.insertBefore(inputGroup, filterButton.nextSibling);
            return;
        }

        // 2. Header (Sammlungen)
        const headerArea = document.querySelector('header, .header, [class*="header"]');
        if (headerArea) {
            const inputGroup = createInputGroup();
            headerArea.appendChild(inputGroup);
            return;
        }

        // 3. Fixed oben rechts
        const inputGroup = createInputGroup(true);
        document.body.appendChild(inputGroup);
    }

    function createInputGroup(isFixed = false) {
        const inputGroup = document.createElement('div');
        inputGroup.id = 'tm-filter-input-group';

        inputGroup.style.cssText = isFixed ?
            `position: fixed; top: 20px; right: 20px; z-index: 9999; display: inline-flex; gap: 4px; align-items: center; padding: 8px 12px; background: rgba(255,255,255,0.95); border-radius: 20px; border: 1px solid #ddd; box-shadow: 0 2px 8px rgba(0,0,0,0.15); font-size: 13px;` :
            `display: inline-flex; gap: 4px; align-items: center; margin-left: 12px; padding: 6px 10px; background: rgba(255,255,255,0.9); border-radius: 16px; border: 1px solid #ddd; font-size: 13px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);`;

        const label = document.createElement('span');
        label.textContent = 'Min: ';
        label.style.cssText = 'color: #666; font-weight: 500; font-size: 12px;';

        const input = document.createElement('input');
        input.type = 'number';
        input.value = MIN_RATINGS;
        input.min = '0';
        input.max = '10000';
        input.style.cssText = `width: 55px; padding: 2px 4px; border: none; background: transparent; text-align: center; font-size: 13px; outline: none;`;

        input.addEventListener('input', (e) => {
            MIN_RATINGS = parseInt(e.target.value) || 0;
            processTiles(document.body);
        });

        inputGroup.append(label, input);
        return inputGroup;
    }

    // Initialisierung
    const init = () => {
        addFilterInput();
        processTiles(document.body);
    };

    setTimeout(init, 1500);

    const observer = new MutationObserver(() => processTiles(document.body));
    observer.observe(document.body, { childList: true, subtree: true });

})();