NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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();
}
})();