Munzee Specials

Show icons in front of the name of your own creatures at https://www.munzee.com/specials/

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

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Munzee Specials
// @namespace    https://greasyfork.org/users/156194
// @version      0.50
// @description  Show icons in front of the name of your own creatures at https://www.munzee.com/specials/
// @author       rabe85
// @match        https://www.munzee.com/specials
// @match        https://www.munzee.com/specials/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=munzee.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_deleteValue
// ==/UserScript==

(function() {
    'use strict';


    // ============================================================================
    // A) Warten auf ein Element (Promise)
    // ============================================================================
    function waitForElm(selector) {
        return new Promise(resolve => {
            const found = document.querySelector(selector);
            if (found) return resolve(found);

            const observer = new MutationObserver(() => {
                const elem = document.querySelector(selector);
                if (elem) {
                    resolve(elem);
                    observer.disconnect();
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }


    // ============================================================================
    // B) Zählen der eigenen Specials
    // ============================================================================
    function countOwnCreatures() {
        const container = document.querySelector(".alert.alert-info");
        if (!container) return 0;
        return container.children.length;
    }


    // ============================================================================
    // C) Counter aktualisieren
    // ============================================================================
    function updateOwnCreaturesCounter() {
        const count = countOwnCreatures();
        const counterSpan = document.getElementById("own_creatures_counter");
        if (counterSpan) {
            counterSpan.textContent = count + " Own Creatures";
        }
    }


    // ============================================================================
    // D) Name aus erstem <a> extrahieren + Dateiname ableiten
    // ============================================================================

    // Beispiele:
    // "Your Yeti Munzee #66"        -> "Yeti"
    // "Your Pimedus #3"             -> "Pimedus"
    // "Your Ghost of Christmas Future Munzee #12" -> "Ghost of Christmas Future"
    function extractCreatureName(text) {
        let cleaned = text.replace(/^Your\s+/i, ""); // "Yeti Munzee #66"
        cleaned = cleaned.replace(/\s+#\d+.*$/i, ""); // "Yeti Munzee"
        cleaned = cleaned.replace(/\s+Munzee$/i, ""); // "Yeti"
        return cleaned.trim();
    }

    function autoFileName(name) {
        return name.toLowerCase().replace(/\s+/g, "") + ".png";
    }

    // Manuelle Overrides für die Fälle, wo der Dateiname nicht automatisch passt
    const creatureIconsOverride = {
        "Unicorn": "theunicorn.png",
    };


    // ============================================================================
    // E) Bild-Existenz prüfen (CORS-sicher über <img>)
    // ============================================================================
    function fileExists(url) {
        return new Promise(resolve => {
            const img = new Image();
            img.onload = () => resolve(true);
            img.onerror = () => resolve(false);
            img.src = url + "?cb=" + Date.now();
        });
    }


    // ============================================================================
    // F) Dynamische Verarbeitung der eigenen Specials (stabil, kein innerHTML)
    // ============================================================================
    const processedOwnSpecials = new WeakSet();

    async function processOwnSpecial(row, url) {
        if (processedOwnSpecials.has(row)) return;
        processedOwnSpecials.add(row);

        const firstLink = row.querySelector("a");
        if (!firstLink) return;

        const text = firstLink.textContent;
        const creatureName = extractCreatureName(text);

        let file = creatureIconsOverride[creatureName] || autoFileName(creatureName);
        const fullUrl = url + file;

        const exists = await fileExists(fullUrl);
        if (!exists) {
            console.warn("Kein Icon gefunden für:", creatureName, "→", file);
            return; // KEIN ICON EINBLENDEN
        }

        const img = document.createElement("img");
        img.src = fullUrl;
        img.alt = creatureName;
        img.title = creatureName;
        img.style.maxHeight = "32px";
        img.style.marginRight = "4px";

        row.insertBefore(img, firstLink);
    }


    // ============================================================================
    // G) Observer für eigene Specials (mit Debounce + parallelem Check)
    // ============================================================================
    let ownSpecialsTimer = null;

    function observeOwnSpecials(url) {
        const container = document.querySelector(".alert.alert-info");
        if (!container) return;

        async function processAll() {
            const rows = Array.from(container.children);
            const tasks = rows.map(row => processOwnSpecial(row, url));
            await Promise.all(tasks);
            updateOwnCreaturesCounter();
        }

        processAll();

        const observer = new MutationObserver(() => {
            clearTimeout(ownSpecialsTimer);
            ownSpecialsTimer = setTimeout(processAll, 50);
        });

        observer.observe(container, {
            childList: true,
            subtree: false
        });
    }


    // ============================================================================
    // H) LEGENDEN – Verarbeitung EINER grid-row (mit Debounce)
    // ============================================================================
    const processedLegendRows = new WeakSet();
    let legendTimer = null;

    function munzee_specials_legend(row) {
        const img = row.querySelector("img");
        if (!img) return;

        const parts = img.src.split("/");
        const filename = parts[parts.length - 1].split(".")[0];
        const label = filename.charAt(0).toUpperCase() + filename.slice(1);

        img.setAttribute("title", label);
        img.setAttribute("alt", label);
    }

    function handleLegendRow(row) {
        if (!processedLegendRows.has(row)) {
            processedLegendRows.add(row);
            munzee_specials_legend(row);
        }
    }

    function observeLegendRows() {
        const container = document.querySelector(".captures-grid-rows");
        if (!container) return;

        function processAll() {
            document.querySelectorAll(".captures-grid-rows .grid-row")
                .forEach(handleLegendRow);
        }

        processAll();

        const observer = new MutationObserver(() => {
            clearTimeout(legendTimer);
            legendTimer = setTimeout(processAll, 50);
        });

        observer.observe(container, {
            childList: true,
            subtree: true
        });
    }


    // ============================================================================
    // I) BUTTON-VERARBEITUNG – EINMAL PRO BUTTON (mit Debounce)
    // ============================================================================
    const processedButtons = new WeakSet();
    let buttonTimer = null;

    function munzee_specials_button(btn) {
        const img = btn.querySelector("img");
        if (!img) return;

        const parts = img.src.split("/");
        const filename = parts[parts.length - 1].split(".")[0];
        const label = filename.charAt(0).toUpperCase() + filename.slice(1);

        img.setAttribute("title", label);
        img.setAttribute("alt", label);
    }

    function handleButton(btn) {
        if (!processedButtons.has(btn)) {
            processedButtons.add(btn);
            munzee_specials_button(btn);
        }
    }

    function observeButtons(selector) {
        function processAll() {
            document.querySelectorAll(selector).forEach(handleButton);
        }

        processAll();

        const observer = new MutationObserver(() => {
            clearTimeout(buttonTimer);
            buttonTimer = setTimeout(processAll, 50);
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }


    // ============================================================================
    // J) INITIALISIERUNG – vollständig integriert
    // ============================================================================
    async function munzee_specials_init() {

        await waitForElm(".captures-grid-rows");

        var munzee_setting_specials_url = GM_getValue('munzee_setting_specials_url', 'v4');

        function save_settings_v3pins() {
            GM_setValue('munzee_setting_specials_url', 'v3');
            location.reload();
        }

        function save_settings_v4pins() {
            GM_setValue('munzee_setting_specials_url', 'v4');
            location.reload();
        }

        var specials_url_link = "";
        var own_specials_map = document.getElementsByClassName('map-wrap')[0]?.parentNode;
        var own_creatures_count = countOwnCreatures();

        if (munzee_setting_specials_url == 'v3') {
            specials_url_link =
                `<div style="font-family: Ubuntu,sans-serif; font-weight: 400; font-style: italic; font-size: 23px; color: #999; border-bottom: 1px solid #999; margin-bottom: 10px; padding-bottom: 30px;">
                <span id="own_creatures_counter" style="float:left;">${own_creatures_count} Own Creatures</span>
                <span style="float:right; cursor: pointer; font-size: small; margin-top: 10px;" id="save_settings_v4pins">Show v4 Pins</span>
            </div>`;
            if (own_specials_map) {
                own_specials_map.insertAdjacentHTML('beforebegin', specials_url_link);
                document.getElementById('save_settings_v4pins').addEventListener("click", save_settings_v4pins, false);
            }
        } else {
            specials_url_link =
                `<div style="font-family: Ubuntu,sans-serif; font-weight: 400; font-style: italic; font-size: 23px; color: #999; border-bottom: 1px solid #999; margin-bottom: 10px; padding-bottom: 30px;">
                <span id="own_creatures_counter" style="float:left;">${own_creatures_count} Own Creatures</span>
                <span style="float:right; cursor: pointer; font-size: small; margin-top: 10px;" id="save_settings_v3pins">Show v3 Pins</span>
            </div>`;
            if (own_specials_map) {
                own_specials_map.insertAdjacentHTML('beforebegin', specials_url_link);
                document.getElementById('save_settings_v3pins').addEventListener("click", save_settings_v3pins, false);
            }
        }

        var url = munzee_setting_specials_url === "v3"
        ? "https://www.otb-server.de/munzee/v3pins/"
        : "https://munzee.global.ssl.fastly.net/images/pins/";

        observeOwnSpecials(url);
        observeLegendRows();
    }


    // ============================================================================
    // K) STARTLOGIK
    // ============================================================================
    function start_munzee_specials() {
        munzee_specials_init();
        observeButtons("button.icon-btn");
    }

    if (document.readyState === "complete" || document.readyState === "interactive") {
        start_munzee_specials();
    } else {
        document.addEventListener("DOMContentLoaded", start_munzee_specials);
    }


})();