Greasy Fork is available in English.
Show icons in front of the name of your own creatures at https://www.munzee.com/specials/
// ==UserScript==
// @name Munzee Specials
// @namespace https://greatest.deepsurf.us/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);
}
})();