Der intelligente Flächen-Preisrechner für Kleinanzeigen 🏡

Zeigt €/m², färbt Anzeigen dynamisch (basierend auf regional gespeicherten Preisen nach 3-stelligem PLZ-Präfix), Seitennavigation mit 'a'/'d'. Mit Löschbestätigung.

// ==UserScript==
// @name         Der intelligente Flächen-Preisrechner für Kleinanzeigen 🏡
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Zeigt €/m², färbt Anzeigen dynamisch (basierend auf regional gespeicherten Preisen nach 3-stelligem PLZ-Präfix), Seitennavigation mit 'a'/'d'. Mit Löschbestätigung.
// @author       Dein Name
// @license MIT
// @icon         https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://kleinanzeigen.de&size=64
// @match        https://www.kleinanzeigen.de/s-wohnung-mieten/*
// @match        https://www.kleinanzeigen.de/s-haus-mieten/*
// @match        https://www.kleinanzeigen.de/s-wohnung-kaufen/*
// @match        https://www.kleinanzeigen.de/s-haus-kaufen/*
// @match        https://www.kleinanzeigen.de/s-wg-zimmer/*
// @match        https://www.kleinanzeigen.de/s-immobilien/*
// @match        https://www.kleinanzeigen.de/s-grundstuecke-gaerten/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const REGIONAL_STORAGE_KEY = 'kleinanzeigenRegionalSqmPrices_3digit';
    const PLZ_PREFIX_LENGTH = 3;
    let globalSqmPricesByRegion = loadRegionalSqmPrices();

    GM_addStyle(`
        .sqm-price-display { font-size: 1.4em; color: #20242C; font-weight: bold; margin-left: 10px; }
        /* ... Rest des CSS unverändert ... */
        .aditem-main--middle--price-shipping { display: flex; align-items: baseline; flex-wrap: wrap; }
        .sqm-price-container { margin-left: auto; padding-left: 5px; }
        .sqm-price-low { background-color: rgba(144, 238, 144, 0.4) !important; }
        .sqm-price-medium { background-color: rgba(255, 210, 100, 0.4) !important; }
        .sqm-price-high { background-color: rgba(255, 127, 127, 0.4) !important; }
        .sqm-price-uniform { background-color: rgba(200, 200, 200, 0.3) !important; }
    `);

    function loadRegionalSqmPrices() {
        return GM_getValue(REGIONAL_STORAGE_KEY, {});
    }

    function saveRegionalSqmPrices() {
        GM_setValue(REGIONAL_STORAGE_KEY, globalSqmPricesByRegion);
    }

    function calculateStorageInfo(data) {
        const jsonString = JSON.stringify(data);
        const sizeBytes = new TextEncoder().encode(jsonString).length;
        const sizeMegabytes = (sizeBytes / (1024 * 1024)).toFixed(4); // Etwas mehr Genauigkeit für kleine MB-Werte

        let totalAdEntries = 0;
        for (const region in data) {
            if (data.hasOwnProperty(region)) {
                totalAdEntries += Object.keys(data[region]).length;
            }
        }
        return { sizeMegabytes, totalAdEntries };
    }

    function confirmAndClearAllRegionalSqmPrices() {
        const currentData = GM_getValue(REGIONAL_STORAGE_KEY, {}); // Aktuelle Daten für Info holen
        const { sizeMegabytes, totalAdEntries } = calculateStorageInfo(currentData);

        const confirmationMessage = `Möchten Sie wirklich alle gespeicherten regionalen m² Preisdaten löschen?\n\n` +
                                  `Anzahl der erfassten Anzeigen: ${totalAdEntries}\n` +
                                  `Geschätzte Speichergröße: ${sizeMegabytes} MB\n\n` +
                                  `Diese Aktion kann nicht rückgängig gemacht werden.`;

        if (confirm(confirmationMessage)) { // confirm() zeigt OK/Abbrechen Dialog
            GM_deleteValue(REGIONAL_STORAGE_KEY);
            globalSqmPricesByRegion = {}; // Internen Speicher auch leeren
            alert('Alle gespeicherten regionalen m² Preise (3-stellig PLZ) wurden gelöscht. Die Farbanzeige wird zurückgesetzt.');
            processAdItems(); // Neu einfärben (jetzt nur basierend auf aktueller Seite oder leer)
        } else {
            alert('Löschvorgang abgebrochen.');
        }
    }

    GM_registerMenuCommand("Alle regionalen m² Preise (3-stellig PLZ) löschen...", confirmAndClearAllRegionalSqmPrices, "L");
    // Der Titel des Menübefehls hat jetzt "..." um anzudeuten, dass eine weitere Interaktion folgt.

    function parsePrice(priceString) { /* ... unverändert ... */ }
    function parseArea(areaString) { /* ... unverändert ... */ }
    function removeColorClasses(element) { /* ... unverändert ... */ }
    function getPlzPrefixFromItem(item) { /* ... unverändert ... */ }
    // Die Funktionen parsePrice, parseArea, removeColorClasses, getPlzPrefixFromItem hier einfügen
    // (aus Platzgründen gekürzt, sind im vorherigen Skript vorhanden)
     function parsePrice(priceString) {
        if (!priceString) return null;
        const cleanedPrice = priceString.replace(/\s*VB\s*/i, '').replace(/\s*€\s*/g, '').replace(/\./g, '').replace(/,/g, '.').trim();
        if (cleanedPrice.toLowerCase() === 'zu verschenken') return 0;
        if (cleanedPrice.toLowerCase() === 'auf anfrage' || cleanedPrice.toLowerCase() === 'anfrage') return null;
        const price = parseFloat(cleanedPrice);
        return isNaN(price) ? null : price;
    }

    function parseArea(areaString) {
        if (!areaString) return null;
        const match = areaString.match(/([\d\.,]+)\s*m²/i);
        if (match && match[1]) {
            const cleanedArea = match[1].replace(/\./g, '').replace(/,/g, '.');
            const area = parseFloat(cleanedArea);
            return isNaN(area) || area === 0 ? null : area;
        }
        return null;
    }

    function removeColorClasses(element) {
        element.classList.remove('sqm-price-low', 'sqm-price-medium', 'sqm-price-high', 'sqm-price-uniform');
    }

    function getPlzPrefixFromItem(item) {
        const locationElement = item.querySelector('.aditem-main--top--left');
        if (locationElement) {
            const locationText = locationElement.textContent;
            const plzMatch = locationText.match(/\b(\d{5})\b/);
            if (plzMatch && plzMatch[1]) {
                return plzMatch[1].substring(0, PLZ_PREFIX_LENGTH);
            }
        }
        return null;
    }


    function processAdItems() { /* ... Logik bleibt weitgehend gleich, lädt und speichert globalSqmPricesByRegion ... */
        globalSqmPricesByRegion = loadRegionalSqmPrices();
        const items = document.querySelectorAll('article.aditem');
        let mainStorageHasChanged = false;
        const itemsByPlzOnPage = {};

        items.forEach(item => {
            removeColorClasses(item);
            const adId = item.dataset.adid;
            const plzPrefix = getPlzPrefixFromItem(item);

            if (!adId || !plzPrefix) {
                return;
            }

            if (!globalSqmPricesByRegion[plzPrefix]) {
                globalSqmPricesByRegion[plzPrefix] = {};
            }
            if (!itemsByPlzOnPage[plzPrefix]) {
                itemsByPlzOnPage[plzPrefix] = [];
            }

            const priceShippingContainer = item.querySelector('.aditem-main--middle--price-shipping');
            if (!priceShippingContainer) return;

            const existingSqmTextContainer = priceShippingContainer.querySelector('.sqm-price-container');
            if (existingSqmTextContainer) existingSqmTextContainer.remove();

            const priceElement = priceShippingContainer.querySelector('.aditem-main--middle--price-shipping--price');
            const priceText = priceElement ? priceElement.textContent : null;
            const tagsContainer = item.querySelector('.aditem-main--bottom p');
            let areaText = null;
            if (tagsContainer) {
                const simpleTags = tagsContainer.querySelectorAll('span.simpletag');
                simpleTags.forEach(tag => { if (tag.textContent && tag.textContent.includes('m²')) areaText = tag.textContent; });
            }

            const price = parsePrice(priceText);
            const area = parseArea(areaText);

            if (price !== null && area !== null && area > 0) {
                const pricePerSqm = price / area;
                const roundedPricePerSqm = Math.round(pricePerSqm);

                if (globalSqmPricesByRegion[plzPrefix][adId] !== roundedPricePerSqm) {
                    globalSqmPricesByRegion[plzPrefix][adId] = roundedPricePerSqm;
                    mainStorageHasChanged = true;
                }

                itemsByPlzOnPage[plzPrefix].push({ element: item, adId: adId, precisePricePerSqm: pricePerSqm });

                const sqmPriceOuterContainer = document.createElement('div');
                sqmPriceOuterContainer.classList.add('sqm-price-container');
                const sqmPriceElement = document.createElement('span');
                sqmPriceElement.classList.add('sqm-price-display');
                sqmPriceElement.textContent = `${pricePerSqm.toFixed(2).replace('.', ',')} €/m²`;
                sqmPriceOuterContainer.appendChild(sqmPriceElement);
                priceShippingContainer.appendChild(sqmPriceOuterContainer);
            }
        });

        if (mainStorageHasChanged) {
            saveRegionalSqmPrices();
        }

        for (const plzKey in itemsByPlzOnPage) {
            const itemsInThisRegionOnPage = itemsByPlzOnPage[plzKey];
            const allStoredPricesInThisRegion = Object.values(globalSqmPricesByRegion[plzKey] || {}).filter(p => typeof p === 'number');

            if (allStoredPricesInThisRegion.length < 1) continue;

            const minRegionalPrice = Math.min(...allStoredPricesInThisRegion);
            const maxRegionalPrice = Math.max(...allStoredPricesInThisRegion);
            const regionalRange = maxRegionalPrice - minRegionalPrice;

            let lowerBound, upperBound;

            if (regionalRange === 0) {
                itemsInThisRegionOnPage.forEach(data => data.element.classList.add('sqm-price-uniform'));
                continue;
            }

            lowerBound = minRegionalPrice + regionalRange / 3;
            upperBound = minRegionalPrice + (regionalRange * 2) / 3;

            itemsInThisRegionOnPage.forEach(data => {
                const itemElement = data.element;
                const roundedPriceForThisAd = globalSqmPricesByRegion[plzKey][data.adId];

                if (typeof roundedPriceForThisAd !== 'number') return;

                if (roundedPriceForThisAd <= lowerBound) {
                    itemElement.classList.add('sqm-price-low');
                } else if (roundedPriceForThisAd <= upperBound) {
                    itemElement.classList.add('sqm-price-medium');
                } else {
                    itemElement.classList.add('sqm-price-high');
                }
            });
        }
    }

    processAdItems();

    // MutationObserver (unverändert)
    const observerTargetNode = document.getElementById('srchrslt-adtable') || document.body;
    // ... (vollständiger MutationObserver Code hier einfügen)
    const observer = new MutationObserver(function(mutationsList) {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                let hasNewAdItems = false;
                for (const addedNode of mutation.addedNodes) {
                    if (addedNode.nodeType === Node.ELEMENT_NODE && (addedNode.matches && (addedNode.matches('.aditem') || addedNode.querySelector('.aditem')))) {
                        hasNewAdItems = true;
                        break;
                    }
                }
                if (hasNewAdItems) {
                    setTimeout(processAdItems, 350);
                    return;
                }
            }
        }
    });
    observer.observe(observerTargetNode, { childList: true, subtree: true });


    // Tastaturkürzel (unverändert)
    document.addEventListener('keydown', function(event) {
    // ... (vollständiger Tastaturkürzel Code hier einfügen)
        const targetTagName = event.target.tagName.toLowerCase();
        if (targetTagName === 'input' || targetTagName === 'textarea' || event.target.isContentEditable) {
            return;
        }
        const key = event.key.toLowerCase();
        if (key === 'a') {
            const prevButton = document.querySelector('.pagination-prev');
            if (prevButton) prevButton.click();
        } else if (key === 'd') {
            const nextButton = document.querySelector('.pagination-next');
            if (nextButton) nextButton.click();
        }
    });

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址