您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Extend features of the stamp album on Grundo's Café
// ==UserScript== // @name Grundo's Café stamp album helper // @namespace github.com/windupbird144 // @version 0.10 // @description Extend features of the stamp album on Grundo's Café // @author supercow64, eleven, rowanberryyyy, kateslines // @match https://www.grundos.cafe/stamps/album/?page_id=* // @icon https://www.grundos.cafe/static/images/favicon.66a6c5f11278.ico // @grant none // @license MIT // ==/UserScript== const prefix = "https://grundoscafe.b-cdn.net/items" function removePrefix(url) { return url.replace(prefix, "") } (function () { 'use strict'; // Fetch the database of stamps remotely. We expect the host of the external resource to manage // the E-Tag header, use no-cache to only reload the resource if it has been update. // // The entry stamp_database in localStorage overwrites the default resource. // This is useful for testing. You can upload an experimental database to e.g. Github Gists, // write the link into localStorage and work with your development version fetch(localStorage.getItem("stamp_database") ?? "https://raw.githubusercontent.com/windupbird144/gc-stamp-album-helper/main/stamps.json", { cache: "no-cache" }) .then(res => res.json()) .then(main) function main(database) { const table = document.querySelector(`#stamp_tbl`) const cells = table.querySelectorAll("td img") // Double click for a shop wizard searchg table.addEventListener('dblclick', e => { // Any element with 'name' in its dataset is considered shop wizard searchable const name = e.target?.dataset?.name if (name) { // A stamp was clicked e.stopPropagation() e.preventDefault() // Open the shop wizard in a new tab searchWizard(name) } }) table.addEventListener('click', e => { const slot = e?.target?.dataset?.position if (typeof slot === "string") { updateInfo(+slot) } }) // The url stores a query parameter page_id=? which indicates the current album const page = +new URLSearchParams(window.location.search).get("page_id") // Update the album slots for (let slot = 0; slot < cells.length; slot++) { // This identifies if we have a stamp, wheteher it is collected and a database entry const cell = cells[slot] const collected = cell.title const databaseEntry = database[page] ? database[page][slot] : undefined // Update the dataset for the shop wizard functionality if (databaseEntry) { cell.dataset.position = slot cell.dataset.name = databaseEntry[0] cell.dataset.rarity = databaseEntry[1] cell.dataset.description = databaseEntry[2] cell.dataset.collected = !!collected } // Uncollected stamp fill the slot with database info if (databaseEntry && !collected) { cell.src = `${prefix}/${databaseEntry[3]}` cell.title = `${databaseEntry[0]} - r${databaseEntry[1]} : ${databaseEntry[2]}` cell.style.opacity = 0.25 } } // Open the url in a new tab and fill the form fields function openAndFill(url, formFields, customCallbacks) { const w = window.open(url) w.addEventListener("DOMContentLoaded", () => { const document = w.document for (let [name, value] of Object.entries(formFields)) { const formField = document.querySelector(`#page_content [name='${name}']`) if (formField) { formField.value = value } } if (customCallbacks) { for (let callback of customCallbacks) { callback(document) } } }) } function encodeQuery(key, value) { const tmp = new URLSearchParams() tmp.set(key, value) return tmp.toString() } const searchWizard = (query) => window.open(`/market/wizard?${encodeQuery("query",query)}`) const searchTradingPost = (query) => openAndFill('/island/tradingpost/browse/', { category : 2, query }) const searchAuctionHouse = () => window.open("/auctions") const searchSDB = (query) => window.open(`/safetydeposit/?page=1&${encodeQuery("query", query)}&category=0`) const searchJellyneo = (query) => window.open(`https://items.jellyneo.net/search/?${encodeQuery("name", query)}`) const searchVirtupets = (query) => window.open(`https://virtupets.net/search?${encodeQuery("q", query)}`) const addToWishlist = (query) => openAndFill( '/wishlist/edit/', { item: query }, [ document => document.querySelector("main details").open = true ] ) const searchShop = () => window.open(`/viewshop/?shop_id=58`) // Show a rich info box at the bottom table.insertAdjacentHTML("beforeend", `<tbody> <tr> <td colspan="5"> <div id="stampinfo" hidden> <div class="name">name</div> <div class="rarity"></div> <div class="cols"> <div class="stamp_arrow" data-delta="-1"><</div> <div class="image"><img src=""/></div> <div class="labels"> <div><label>Position: </label><span class="position"></span></div> <div><label>Status: </label><span class="status"></span></div> <div class="links"> <img width="24" data-search="wizard" src="https://neopialive.s3.us-west-1.amazonaws.com/misc/wiz.png" /> <img width="24" data-search="trading" src="https://neopialive.s3.us-west-1.amazonaws.com/misc/tp.png" /> <img width="24" data-search="auction-house" src="https://i.ibb.co/vYzmPxV/auction25.gif" /> <img width="24" data-search="sdb" src="https://neopialive.s3.us-west-1.amazonaws.com/misc/sdb.gif" /> <img width="24" data-search="jn" src="https://i.ibb.co/cvGsCw4/fishnegg25.gif" /> <img width="24" data-search="virtupets" src="https://virtupets.net/assets/images/vp.png" /> <img width="24" data-search="shop" src="https://grundoscafe.b-cdn.net/misc/shopkeeper/58.gif" /> <img width="24" data-search="wishlist" alt="Add to wishlist" src="https://grundoscafe.b-cdn.net/searchicons/wish_add_green.png" /> </div> </div> <div class="stamp_arrow" data-delta="1">></div> </div> </div> </td> </tr> </tbody><style> #stampinfo { margin-top: 1em; padding: 1em; border: 1px solid #aaa; } #stampinfo .stamp_arrow { font-size: 2em; display: flex; align-items: center; user-select: none; cursor: pointer; } #stampinfo > div { text-align: center; } #stampinfo .labels { text-align: left; display: grid; row-gap: 0.5em; } #stampinfo .image { padding: 0 2em 0 1em; user-select: none; } #stampinfo label, #stampinfo .name { font-weight: bold; } .cols { display: grid; grid-template-columns: min-content auto 1fr min-content; } #compare-user { margin-top: 1em; } [data-collected="true"] { color: darkgreen } [data-collected="false"] { color: darkred } #stamp_tbl td { position: relative; } [data-diff]:after { position: absolute; content: ""; border: 1px solid #aaa; height: 10px; width: 10px; left: 5px; top: 5px; } [data-diff=""]::after { display: none; } [data-diff="minus"]::after { background: rgba(255,0,0,0.7); } [data-diff="plus"]::after { background: rgba(0,255,0,0.7); } </style>`) const stampinfo = table.querySelector("#stampinfo") const infos = { img: stampinfo.querySelector("img"), name: stampinfo.querySelector(".name"), rarity: stampinfo.querySelector(".rarity"), position: stampinfo.querySelector(".position"), status: stampinfo.querySelector(".status") } let currentPos = 0 function updateInfo(pos) { const stampImage = cells[pos] if (!stampImage) return const { src, dataset } = stampImage if (!dataset) return const { name, rarity, collected } = dataset if (!name) return infos.img.src = src infos.name.textContent = name infos.rarity.textContent = "r" + rarity infos.position.textContent = pos + 1 infos.status.textContent = collected === "true" ? "collected" : "not collected" infos.status.dataset.collected = collected stampinfo.hidden = false currentPos = pos return true } stampinfo.addEventListener("click", (e) => { // Move left or right to the next stamp, skipping over empty slots let delta = parseInt(e?.target?.dataset?.delta, 10) if (Math.abs(delta) !== 1) return; let target = currentPos + delta while (true) { if (updateInfo(target)) break; // returns true if the info was updated if (target < 0) break; if (target > 25) break; target = target + delta } }) stampinfo.addEventListener("click", (e) => { const search = e.target.dataset.search const query = cells[currentPos].dataset.name const searchFunction = { "wizard": searchWizard, "trading": searchTradingPost, "auction-house": searchAuctionHouse, "sdb": searchSDB, "virtupets" : searchVirtupets, "jn": searchJellyneo, "shop" : searchShop, "wishlist" : addToWishlist }[search] if (searchFunction) { return searchFunction(query) } }) const jellyneoLinks = { [1]: "/mystery-island-album-avatar-list/", [2]: "/virtupets-album-avatar-list/", [3]: "/tyrannia-album-avatar-list/", [4]: "/haunted-woods-album-avatar-list/", [5]: "/neopia-central-album-avatar-list/", [6]: "/neoquest-album-avatar-list/", [7]: "/snowy-valley-album-avatar-list/", [8]: "/meridell-vs-darigan-album-avatar-list/", [9]: "/lost-desert-album-avatar-list/", [10]: "/battledome-album-avatar-list/", [12]: "/battle-for-meridell-album-avatar-list/", [13]: "/neoquest-ii-album-avatar-list/" } const jellyneoLink = jellyneoLinks[page] if (jellyneoLink) { table.nextElementSibling?.insertAdjacentHTML("afterend", `<a href="https://items.jellyneo.net/search${jellyneoLink}" target="_blank"/><center><img src="https://i.ibb.co/cvGsCw4/fishnegg25.gif" /> Album info <img src="https://i.ibb.co/cvGsCw4/fishnegg25.gif" /></center></a>`) } // Show diff form const compareUser = localStorage.getItem("compare-user") ?? "" table.nextElementSibling.insertAdjacentHTML("beforeend", `<form action="#" id="compare-user"> <label for="compare-user">Compare against another user</label><br> <input type="text" name="compare-user" value="${compareUser}" /> <input type="submit" value="Compare" /> <button name="clear">Clear</button> <div class="error"></div> </form>`) const diff = table.parentElement.querySelector("#compare-user") const error = diff.querySelector(".error") const setError = (msg) => error.textContent = msg const clearError = () => error.textContent = "" // Read the key compare-user from localstorage, make a fetch request to their stamp album and run the diff function function applyDiffHTTP(username) { return fetch(`/stamps/album/?page_id=${page}&owner=${username}`) .then(res => res.text()) .then((html) => { if (html.includes("That user does not exist!")) { throw new Error("That user user does not exist!") } else { applyDiff(html) } }) } // Change the name in the compare-user form field, save it to local storage and run applyDiffFromLocalStorage immediately table.parentElement.addEventListener("submit", async (e) => { e.preventDefault() let username = diff.querySelector(`[name="compare-user"]`).value.trim() if (!username?.length) { setError("Please enter a valid username") return } setError("Loading...") applyDiffHTTP(username) .then(() => { clearError() localStorage.setItem("compare-user", username) }) .catch((err) => { setError(err.message) }) }) // Stop comparing against another user table.parentElement.addEventListener("click", (e) => { if (e.target.name !== "clear") return e.stopPropagation() e.preventDefault() localStorage.removeItem("compare-user"); const username = diff.querySelector(`[name="compare-user"]`) if (username) { username.value = "" } cells.forEach((cell) => { cell.parentElement.dataset.diff = "" }) }) function applyDiff(html) { // regex to get all stamp images on this html page // match(/src="\/images.+?"/g).map(e => e.match(/\/images.+\.\w+/)[0]) for (let cell of cells) { cell.parentElement.dataset.diff = "" const have = cell.dataset.collected === "true" const otherHas = html.includes(removePrefix(cell.src)) if (have && !otherHas) { cell.parentElement.dataset.diff = "plus" } else if (!have && otherHas) { cell.parentElement.dataset.diff = "minus" } } } if (compareUser) { applyDiffHTTP(compareUser) } } })();