crunchy_soup / Neopets Inventory Price Injector

// ==UserScript==
// @name        Neopets Inventory Price Injector
// @namespace   http://tampermonkey.net/
// @version     1.0.5
// @description Injects ItemDB Market Price and Restock Range into the item details pop-up on the Neopets Inventory page. The price titles are plain black links to the ItemDB page.
// @author      Logan Bell
// @match       https://www.neopets.com/inventory.phtml
// @connect     itemdb.com.br
// @grant       GM_xmlhttpRequest
// @run-at      document-end
// @license     MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const ITEMDB_BASE_URL = "https://itemdb.com.br/item/";

    // --- GUI: Status Box for Debugging ---
    const statusBox = document.createElement('div');
    statusBox.id = 'gemini-status-box';
    statusBox.style.cssText = `
        position: fixed; bottom: 10px; right: 10px; padding: 6px 10px;
        background: #e0f7fa; border: 1px solid #b2ebf2; z-index: 9999;
        font-size: 11px; font-weight: bold; border-radius: 4px; color: #006064;
        box-shadow: 0 2px 5px rgba(0,0,0,0.2);
    `;
    statusBox.innerText = 'NeoScanner waiting to help...';
    document.body.appendChild(statusBox);
    console.log("ItemDB Injector V1.5: Script started.");

    // --- Helper Functions ---

    function updateStatus(text, color = '#006064', background = '#e0f7fa') {
        statusBox.innerText = text;
        statusBox.style.color = color;
        statusBox.style.background = background;
    }

    /** Creates a URL slug from the item name for ItemDB. */
    function createSlug(name) {
        return name.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
    }

    /** Extracts all necessary data from the ItemDB HTML response. */
    function extractPricesFromHTML(html) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');

        let marketPrice = null;
        let restockMin = null;
        let restockMax = null;
        let restockShopLink = null; // New variable to store the shop URL

        // Market Price Extraction (No change needed)
        const marketPriceElement = doc.querySelector('.css-1kdqswr .chakra-stat__number');
        if (marketPriceElement) {
            const priceText = marketPriceElement.textContent.replace(/[^\d]/g, '');
            marketPrice = parseInt(priceText, 10) || null;
        }

        // --- Restock Price Range Extraction (Robust, from v1.0.4) ---
        const restockTitles = doc.querySelectorAll('.css-ztobn h3');
        let restockPriceElement = null;

        for (const title of restockTitles) {
            if (title.textContent.trim() === 'Restock Price') {
                const priceContainer = title.nextElementSibling;
                if (priceContainer) {
                    restockPriceElement = priceContainer.querySelector('p:nth-child(2)');
                    break;
                }
            }
        }

        if (restockPriceElement) {
            const rangeText = restockPriceElement.textContent;
            const parts = rangeText.match(/(\d[\d,]*)\s*NP\s*-\s*(\d[\d,]*)\s*NP/);
            if (parts && parts.length === 3) {
                restockMin = parseInt(parts[1].replace(/,/g, ''), 10) || null;
                restockMax = parseInt(parts[2].replace(/,/g, ''), 10) || null;
            }
        }
        // -------------------------------------------------------------

        // --- Restock Shop Link Extraction (NEW for v1.0.5) ---
        // Find the 'Find At' container
        const findAtContainer = doc.querySelector('.css-172f9ra .css-1a9obp4');

        if (findAtContainer) {
            // Find the link whose child image has the alt text 'Restock Shop'
            const restockLinkElement = findAtContainer.querySelector('a[href*="objects.phtml?type=shop"]');

            if (restockLinkElement) {
                restockShopLink = restockLinkElement.href;
            }
        }
        // ---------------------------------------------------


        return { marketPrice, restockMin, restockMax, restockShopLink }; // Return the new link
    }

    /**
     * Injects the market and restock data into the item details pop-up table.
     * @param {string} itemDBLink - The URL to the item's page on ItemDB.
     * @param {string} restockShopLink - The direct URL to the restock shop.
     */
    function injectPricesIntoPopup(itemDBLink, marketPrice, restockMin, restockMax, restockShopLink) {
        const grid = document.querySelector('.inv-itemStat-grid');

        if (!grid) {
            console.error('Could not find .inv-itemStat-grid to inject prices.');
            return;
        }

        // Remove old injected prices to prevent duplicates when opening multiple items
        grid.querySelectorAll('.injected-stat').forEach(el => el.remove());
        grid.querySelectorAll('.injected-link').forEach(el => el.remove());


        // Helper to create a linked title
        // Now accepts an optional customLink
        function createLinkedTitle(text, customLink = itemDBLink) {
            const link = document.createElement('a');
            link.href = customLink; // Use customLink if provided, otherwise use itemDBLink
            link.target = '_blank';
            link.className = 'inv-itemStat injected-link';
            // Custom styling for plain black text without underline
            link.style.cssText = 'color: #000000; text-decoration: none; font-weight: bold;';
            link.textContent = text;
            return link;
        }

        // --- Market Price Link and Value Injection (Keeps itemDB link) ---

        const marketPriceLink = createLinkedTitle('Market Price', itemDBLink);
        grid.appendChild(marketPriceLink);

        const marketPriceSpan = document.createElement('span');
        marketPriceSpan.className = 'inv-itemStat-num injected-stat';
        if (marketPrice !== null) {
             marketPriceSpan.textContent = marketPrice.toLocaleString('en-US') + ' NP';
             marketPriceSpan.title = 'Source: itemdb.com.br';
             marketPriceSpan.style.color = '#388e3c'; // Green color for price value
        } else {
             marketPriceSpan.textContent = 'N/A';
             marketPriceSpan.style.color = '#d32f2f'; // Red for error/not found
        }
        grid.appendChild(marketPriceSpan);


        // --- Restock Range Link and Value Injection (Uses restockShopLink) ---

        // Determine the link to use: the specific shop link if found, otherwise fall back to itemDB
        const finalRestockLink = restockShopLink || itemDBLink;

        const restockLink = createLinkedTitle('Restock Range', finalRestockLink);
        grid.appendChild(restockLink);

        const restockSpan = document.createElement('span');
        restockSpan.className = 'inv-itemStat-num injected-stat';
        if (restockMin !== null && restockMax !== null) {
             restockSpan.textContent = `${restockMin.toLocaleString('en-US')} - ${restockMax.toLocaleString('en-US')} NP`;
             restockSpan.title = `Source: itemdb.com.br. Link to Neopets shop.`;
             restockSpan.style.color = '#1976d2'; // Blue color for price value
        } else {
             restockSpan.textContent = 'N/A';
             restockSpan.style.color = '#d32f2f'; // Red for error/not found
        }
        grid.appendChild(restockSpan);


        updateStatus("NeoScanner priced successfully!", 'green', '#d4edda');
        setTimeout(() => statusBox.remove(), 5000); // Remove status box after 5 seconds for a cleaner look
    }

    /**
     * The main processing function triggered when the item pop-up is shown.
     */
    function checkAndInjectPrice() {
        // 1. Get the item name from the pop-up header
        const itemNameElement = document.getElementById('invItemName');
        if (!itemNameElement) {
            console.log('Pop-up is open, but item name element not found.');
            return;
        }

        const itemName = itemNameElement.textContent.trim();
        if (!itemName) {
            updateStatus("ItemDB Injector: Item name not found in pop-up.", 'red', '#f8d7da');
            return;
        }

        updateStatus(`ItemDB Injector: Checking price for "${itemName}"...`);

        const itemSlug = createSlug(itemName);
        const itemDBLink = ITEMDB_BASE_URL + itemSlug;

        // 2. Fetch ItemDB Data
        GM_xmlhttpRequest({
            method: "GET",
            url: itemDBLink,
            onload: function(response) {
                if (response.status !== 200) {
                    console.error(`ItemDB lookup failed for ${itemName}: ${response.statusText}`);
                    updateStatus(`ItemDB Injector: Price check failed (${response.status})`, 'red', '#f8d7da');
                    injectPricesIntoPopup(itemDBLink, null, null, null, null); // Pass null for shop link
                    return;
                }

                const { marketPrice, restockMin, restockMax, restockShopLink } = extractPricesFromHTML(response.responseText);

                // 3. Inject results into the pop-up
                injectPricesIntoPopup(itemDBLink, marketPrice, restockMin, restockMax, restockShopLink);
                console.log(`✅ Injected prices for "${itemName}" - Market: ${marketPrice}, Restock: ${restockMin}-${restockMax} (Shop: ${restockShopLink || 'N/A'})`);
            },
            onerror: function(err) {
                console.error(`Request Failed for ${itemName}:`, err);
                updateStatus("ItemDB Injector: Network request failed.", 'red', '#f8d7da');
                injectPricesIntoPopup(itemDBLink, null, null, null, null); // Pass null for shop link
            }
        });
    }

    // --- Observer Setup (Main Logic for Inventory) ---

    // The item pop-up element
    const popup = document.getElementById('invDesc');

    if (!popup) {
        updateStatus("ItemDB Injector: Item pop-up element not found. Script may fail.", 'red', '#f8d7da');
        console.error('The item pop-up element #invDesc was not found.');
        return;
    }

    // Create a MutationObserver to watch for changes to the pop-up's style (when it becomes visible)
    const observer = new MutationObserver(function(mutationsList, observer) {
        for (const mutation of mutationsList) {
            if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
                const currentDisplay = window.getComputedStyle(popup).display;
                if (currentDisplay === 'block') {
                    // Pop-up has just been opened/shown
                    checkAndInjectPrice();
                }
            }
        }
    });

    // Start observing the target node for changes in attributes (specifically 'style')
    observer.observe(popup, { attributes: true, attributeFilter: ['style'] });

    // Initial check in case the pop-up is already visible on script load (unlikely, but safe)
    if (window.getComputedStyle(popup).display === 'block') {
        checkAndInjectPrice();
    }

})();