您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a dedicated tag voting panel to https://namemc.com/privacy#vote
// ==UserScript== // @name Power Tag // @namespace https://bunnynabbit.com // @version 1.6.1 // @description Adds a dedicated tag voting panel to https://namemc.com/privacy#vote // @author BunnyNabbit // @license MIT // @match *.namemc.com/privacy // @match namemc.com/privacy // @icon https://bunnynabbit.com/powertag2.png // @grant GM.xmlHttpRequest // @connect s.namemc.com // @contributionURL https://ko-fi.com/bunnynabbit // @supportURL https://t.me/BunnyNabbit // ==/UserScript== (async function () { 'use strict'; const mainBody = document.querySelector("body > main") if (window.location.hash !== "#vote") { const button = createElement("button") button.innerText = "Looking for Power Tag? Click here." button.onclick = () => { window.location.hash = "#vote" window.location.reload() } mainBody.prepend(button) return } const pendingSkins = [] document.querySelector("head > title").innerText = "Power Tag | NameMC" // Template HTML from NameMC let selectModeTemplate = '<div name="modes" class="card mb-3"><div class="card-header py-1"><strong uses="">Select mode</strong></div><div class="row no-gutters align-items-center border-top p-1 px-3"></div><button class="btn btn-secondary btn-sm" name="survey">Review pending tags</button><button class="btn btn-secondary btn-sm" name="search">Search and review approved tags</button><button class="btn btn-secondary btn-sm" name="trending">Review tags on trending skins</button><button class="btn btn-secondary btn-sm" name="random">Review random skins</button></div>' let cardMarkdownTemplate = '<div class="card-header py-1"><strong uses>1 uses</strong></div><div class="card-body checkered p-1 px-3"><a href="/skin/1cadce38d109c22f" target="_blank"><div class="card-body text-center p-1"><img class="drop-shadow auto-size" loading="lazy" width="320" height="160" src=""></div></a></div><div quik="votes"></div><div class="row no-gutters align-items-center border-top p-1 px-3"></div><button class="btn btn-secondary btn-sm" quik="skip">Skip</button>' let voteMarkdownTemplate = '<div class="row no-gutters align-items-center border-top p-1 px-3"><div class="col-auto m-1 text-nowrap"><span quik="status">Status:</span> Is <a style="border-radius: 1rem; max-width: 100%; font-weight: bold" class="text-nowrap text-ellipsis btn btn-sm btn-primary" quik="tag" href="" target="_blank">Blue Eyes</a> a good tag?<a quik="google" href="" target="_blank" rel="nofollow" title="Image Search (Google)"><img class="emoji" draggable="false" alt="🖼️" src="https://s.namemc.com/img/frame-with-picture.png"></a><a quik="bing" href="" target="_blank" rel="nofollow" title="Image Search (Bing)"><img style="filter: hue-rotate(150deg);" class="emoji" draggable="false" alt="🖼️" src="https://s.namemc.com/img/frame-with-picture.png"></a></div><div quik="voteButtons" class="col m-1 text-nowrap text-right"><button class="btn btn-outline-success btn-sm" type="submit" name="vote" value="1"><i class="fas fa-thumbs-up"></i></button><button class="btn btn-outline-danger btn-sm" type="submit" name="vote" value="-1"><i class="fas fa-thumbs-down"></i></button></div></div>' const references = [ ["ears mod", "Ears is a mod that adds ears, snouts, tails, horns, wings, and more to the player. These features are embedded in the skin file as unused space, you can 🔽 the skin from NameMC and use it with the Ears mod. NameMC does not render Ears data, so you must go to https://ears.unascribed.com/manipulator/ and check if this skin does contain valid Ears data."], ] function getReference(tag, details) { console.log({tag}) let reference = references.find(entry => tag.toLowerCase() == entry[0]) if (reference) { reference = reference[1] reference = reference.replace("🔽", `<a target="_blank" href="https://s.namemc.com/i/${details.hash}.png">download</a>`) console.log(reference) return reference } return null } function clearPage() { mainBody.innerHTML = "" } function selectModePage() { mainBody.innerHTML = selectModeTemplate const selectMode = mainBody.children.modes selectMode.children.survey.onclick = () => { clearPage() modeSurvey() } selectMode.children.search.onclick = () => { const tag = prompt("Search tag") if (tag) { clearPage() modeSearch(`tag/${tag}`) } } selectMode.children.trending.onclick = () => { clearPage() modeSearch(`trending`) } selectMode.children.random.onclick = () => { clearPage() modeSearch(`random`) } } function createElement(tag, className = "") { const element = document.createElement(tag) element.className = className return element } function getFrontAndBackImage(skin, model) { return `https://s.namemc.com/3d/skin/body.png?id=${skin}&model=${model}&width=320&height=201&front_and_back=true` } function cleanSkinUrl(url) { return url.replace("https://namemc.com/skin/", "").replace(location.origin + /skin/, "") } function changeTag(el, newTagName, keepAttributes = true) { // https://gist.github.com/Daniel-Hug/d245b53b6195b9596c00659cb17cda6c var newEl = document.createElement(newTagName) // Copy the children while (el.firstChild) { newEl.appendChild(el.firstChild) // *Moves* the child } // Copy the attributes if (keepAttributes) { for (var i = el.attributes.length - 1; i >= 0; --i) { newEl.attributes.setNamedItem(el.attributes[i].cloneNode()) } } // Replace it el.parentNode.replaceChild(newEl, el) return newEl } function getSkinDetails(skin = "31cfd17dee5fb3b0", datum) { // Collect some valid data of the skin function processData(data) { const voteElements = data.querySelectorAll("#tag-dialog > div > div > div.modal-body.border-bottom.p-0 > div > table > tbody > tr") const tags = [] for (let index = 0; index < voteElements.length; index++) { const element = voteElements[index].children tags.push({ status: element[0].innerText.trim(), name: element[1].innerText.trim(), disabled: element[1].children[0].disabled, votes: parseInt(element[2].innerText.trim().replace("−", "-")), upHilight: !element[3].children.vote.className.includes("outline"), downHilight: !element[4].children.vote.className.includes("outline") }) } return { hash: skin, randomSkin: cleanSkinUrl(data.querySelector("a[href^=\"/skin/\"] div").parentElement.href), usersWearing: parseInt(data.querySelector("strong").innerText.match(/\d/g)?.join('') ?? 0, 10), model: data.querySelector("canvas").getAttribute("data-model"), hasEars: hasEarsData(skin), // promise tags: tags } } function fetchData(resolve) { fetch(`${location.origin}/skin/${skin}`) .then(response => response.text()) .then(str => new window.DOMParser().parseFromString(str, "text/html")) .then(async data => { if (data.body.innerHTML.includes("Error: 429 (Too Many Requests)")) { console.log("429 detected, stalling") await sleep(2000) fetchData(resolve) } resolve(processData(data)) }) } if (datum) { return processData(datum) } else return new Promise(resolve => fetchData(resolve)) } function hasEarsData(hash) { return new Promise(resolve => { GM.xmlHttpRequest({ url: `https://s.namemc.com/i/${hash}.png`, responseType: "arraybuffer", onload: function (response) { const loadImage = new Image() loadImage.src = URL.createObjectURL(new Blob([this.response])) loadImage.onload = ev => { const canvas = document.createElement("canvas") canvas.width = loadImage.width canvas.height = loadImage.height const ctx = canvas.getContext("2d") ctx.drawImage(loadImage, 0, 0) // checks if the skin contains the "magic pixels" const pixel = ctx.getImageData(0, 32, 1, 1).data resolve(pixel[0] == 63 && pixel[1] == 35 && pixel[2] == 216 || pixel[0] == 234 && pixel[1] == 37 && pixel[2] == 1) } }, onerror: function () { resolve(null) } }) }) } function createCard(details, replaceElement, next) { let rediv = createElement("div") // to refresh if (replaceElement) { replaceElement.innerText = "" rediv = replaceElement } const cardElement = createElement("div", "card mb-3") cardElement.innerHTML = cardMarkdownTemplate cardElement.querySelector("a > div > img").src = getFrontAndBackImage(details.hash, details.model) const cardHeader = cardElement.querySelector("strong[uses]") cardHeader.innerText = `${details.usersWearing} wearing | Model: ${details.model}` details.hasEars.then((itDoes) => { if (itDoes) cardHeader.innerHTML += ` | <span title="The skin likely has embedded data for the Ears mod.">Has Ears data</span>` }) cardElement.querySelector("a").href = `${location.origin}/skin/${details.hash}` const tagContainer = cardElement.querySelector("div[quik=\"votes\"]") details.tags.forEach((tag) => { let tagElement = createElement("div") tagElement.innerHTML = voteMarkdownTemplate // maybe use "name" instead of "quik"? a bunch of the DOM API supports using that and is much cleaner. tagContainer.append(tagElement) if (tag.disabled) { changeTag(tagElement.querySelector("[quik=\"tag\"]"), "button") tagElement.querySelector("[quik=\"tag\"]").disabled = tag.disabled } tagElement.querySelector("[quik=\"status\"]").innerText = `${tag.status} (${tag.votes}) |` tagElement.querySelector("[quik=\"tag\"]").innerText = tag.name tagElement.querySelector("[quik=\"tag\"]").href = `/minecraft-skins/tag/${tag.name}` const voteButtons = tagElement.querySelector("[quik=\"voteButtons\"]").children const voteAmounts = [1, -1] if (tag.upHilight) { voteButtons[0].classList.remove("btn-outline-success") voteButtons[0].classList.add("btn-success") voteAmounts[0] = 0 } if (tag.downHilight) { voteButtons[1].classList.remove("btn-outline-danger") voteButtons[1].classList.add("btn-danger") voteAmounts[1] = 0 } async function postVote(amount) { fetch(`${location.origin}/skin/${details.hash}`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ "task": "vote-tag", "tag": tag.name, "vote": `${amount}` }) }) .then(response => response.text()) .then(str => new window.DOMParser().parseFromString(str, "text/html")) .then(async (data) => { totalVotes++ let newCardDetails = null let remove = true try { newCardDetails = getSkinDetails(details.hash, data || true) for (let index = 0; index < newCardDetails.tags.length; index++) { const other = newCardDetails.tags[index] if (!other.upHilight && !other.downHilight) remove = false } } catch (error) { alert("Error. Try again later?") console.log(error) return //rediv.remove() } if (remove) { rediv.remove() next(details, "allVoted") } else createCard(newCardDetails, rediv, next) // refresh card with new voting data }) } voteButtons[0].onclick = () => { postVote(voteAmounts[0]) } voteButtons[1].onclick = () => { postVote(voteAmounts[1]) } tagElement.querySelector("[quik=\"google\"]").href = `https://www.google.com/search?q=${tag.name}&tbm=isch&safe=active` tagElement.querySelector("[quik=\"bing\"]").href = `https://www.bing.com/images/search?q=${tag.name}` const reference = getReference(tag.name, details) console.log(reference) if (reference) { let referenceElement = createElement("p", "col-auto m-1 px-3") referenceElement.innerHTML = reference tagElement.append(referenceElement) } }) cardElement.querySelector("[quik=\"skip\"]").onclick = async () => { rediv.remove() next(details, "skip") } rediv.append(cardElement) return rediv } selectModePage() let totalVotes = 0 function breakHandler() { totalVotes = 0 let breakTime = 0 let breakTimeInterval = setInterval(() => { if (mainBody.innerText === "") { breakTime++ if (breakTime > 3) { clearInterval(breakTimeInterval) selectModePage() } } else { breakTime = 0 } }, 1000) } async function modeSurvey() { async function next(details, action) { let randomSkin = await getSkinDetails(details.randomSkin) mainBody.append(createCard(randomSkin, null, next)) } let result = await getSkinDetails() for (let i = 0; i < 2; i++) { result = await getSkinDetails(result.randomSkin) mainBody.append(createCard(result, null, next)) } breakHandler() } function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms) }) } async function modeSearch(feature) { const cache = [] const scrapeTime = 8000 const maxVisible = 2 async function next(details, action) { let cachedSkin = cache[0] cache.shift() if (!cachedSkin) return cachedSkin = await getSkinDetails(cachedSkin) mainBody.append(createCard(cachedSkin, null, next)) } async function getPage(page) { fetch(`https://namemc.com/minecraft-skins/${feature}?page=${page}`) .then(response => response.text()) .then(str => new window.DOMParser().parseFromString(str, "text/html")) .then(async (data) => { const skinElements = data.querySelectorAll('a[href*="/skin/"]') for (let i = 0; i < skinElements.length; i++) { const skinElement = skinElements[i] cache.push(cleanSkinUrl(skinElement.href)) } if (skinElements.length < 30) return await sleep(scrapeTime) getPage(page + 1) }) } let populateTime = 0 setInterval(() => { const cardCount = mainBody.querySelectorAll("strong[uses]").length if (cardCount < maxVisible) { populateTime++ } else populateTime = 0 if (populateTime > 5) { populateTime = 0 next(null, "populate") } }, 400) getPage(1) breakHandler() } })()