Grundo's Café stamp album helper

Extend features of the stamp album on Grundo's Café

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Grundo's Café stamp album helper
// @namespace    github.com/windupbird144
// @version      0.11
// @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]?.[slot]?.[0] ? 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) {
                if (!cell.dataset.name) continue
                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)
        }
    }
})();