// ==UserScript== // @name Bazaars in Item Market 2.0, powered by TornPal & IronNerd // @namespace http://tampermonkey.net/ // @version 1.2 // @description Displays bazaar listings with sorting controls via TornPal & IronNerd // @match https://www.torn.com/page.php?sid=ItemMarket* // @grant GM_xmlhttpRequest // @connect tornpal.com // @connect www.ironnerd.me // @run-at document-end // @downloadURL none // ==/UserScript== (function() { 'use strict'; dailyCleanup(); let allDollarToggles = []; const CACHE_DURATION_MS = 60000; let currentSortKey = "price", currentSortOrder = "asc"; let showDollarItems = localStorage.getItem("showDollarItems") === "true"; function setStyles(el, styles) { Object.assign(el.style, styles); } function isDarkMode() { return document.body.classList.contains('dark-mode') || document.body.getAttribute('data-dark-mode') === 'true'; } function getButtonStyle() { return { padding: '2px 4px', border: isDarkMode() ? '1px solid #444' : '1px solid #ccc', borderRadius: '2px', backgroundColor: isDarkMode() ? '#1a1a1a' : '#fff', color: isDarkMode() ? '#fff' : '#000', cursor: 'pointer' }; } function fetchJSON(url, callback) { GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(response) { try { callback(JSON.parse(response.responseText)); } catch(e) { callback(null); } }, onerror: function() { callback(null); } }); } const style = document.createElement("style"); style.textContent = ` @keyframes popAndFlash { 0% { transform: scale(1); background-color: rgba(0, 255, 0, 0.6); } 50% { transform: scale(1.05); } 100% { transform: scale(1); background-color: inherit; } } .pop-flash { animation: popAndFlash 0.8s ease-in-out forwards; } .green-outline { border: 3px solid green !important; } `; document.head.appendChild(style); function getCache(itemId) { try { const key = "tornBazaarCache_" + itemId; const cached = localStorage.getItem(key); if (cached) { const payload = JSON.parse(cached); if (Date.now() - payload.timestamp < CACHE_DURATION_MS) return payload.data; } } catch(e) {} return null; } function setCache(itemId, data) { try { localStorage.setItem("tornBazaarCache_" + itemId, JSON.stringify({ timestamp: Date.now(), data })); } catch(e) {} } function getRelativeTime(ts) { const diffSec = Math.floor((Date.now() - ts * 1000) / 1000); if (diffSec < 60) return diffSec + 's ago'; if (diffSec < 3600) return Math.floor(diffSec/60) + 'm ago'; if (diffSec < 86400) return Math.floor(diffSec/3600) + 'h ago'; return Math.floor(diffSec/86400) + 'd ago'; } function openModal(url) { const originalOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; const modalOverlay = document.createElement('div'); modalOverlay.id = 'bazaar-modal-overlay'; setStyles(modalOverlay, { position: 'fixed', top: '0', left: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0, 0, 0, 0.7)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: '10000', overflow: 'visible' }); const modalContainer = document.createElement('div'); modalContainer.id = 'bazaar-modal-container'; setStyles(modalContainer, { backgroundColor: '#fff', border: '8px solid #000', boxShadow: '0 0 10px rgba(0,0,0,0.5)', borderRadius: '8px', position: 'relative', resize: 'both' }); const savedSize = localStorage.getItem('bazaarModalSize'); if (savedSize) { try { const { width, height } = JSON.parse(savedSize); modalContainer.style.width = (width < 200 ? '80%' : width + 'px'); modalContainer.style.height = (height < 200 ? '80%' : height + 'px'); } catch(e) { modalContainer.style.width = modalContainer.style.height = '80%'; } } else { modalContainer.style.width = modalContainer.style.height = '80%'; } const closeButton = document.createElement('button'); closeButton.textContent = '×'; setStyles(closeButton, Object.assign({}, getButtonStyle(), { position: 'absolute', top: '-20px', right: '-20px', width: '40px', height: '40px', backgroundColor: '#ff0000', color: '#fff', border: 'none', borderRadius: '50%', fontSize: '24px', boxShadow: '0 0 5px rgba(0,0,0,0.5)' })); closeButton.addEventListener('click', () => { modalOverlay.remove(); document.body.style.overflow = originalOverflow; }); modalOverlay.addEventListener('click', e => { if (e.target === modalOverlay) { modalOverlay.remove(); document.body.style.overflow = originalOverflow; } }); const iframe = document.createElement('iframe'); setStyles(iframe, { width: '100%', height: '100%', border: 'none' }); iframe.src = url; modalContainer.append(closeButton, iframe); modalOverlay.appendChild(modalContainer); document.body.appendChild(modalOverlay); if (window.ResizeObserver) { const observer = new ResizeObserver(entries => { for (let entry of entries) { const { width, height } = entry.contentRect; localStorage.setItem('bazaarModalSize', JSON.stringify({ width: Math.round(width), height: Math.round(height) })); } }); observer.observe(modalContainer); } } function createInfoContainer(itemName, itemId) { const dark = isDarkMode(); const container = document.createElement('div'); container.id = 'item-info-container'; container.setAttribute('data-itemid', itemId); setStyles(container, { backgroundColor: dark ? '#2f2f2f' : '#f9f9f9', color: dark ? '#ccc' : '#000', fontSize: '13px', border: dark ? '1px solid #444' : '1px solid #ccc', borderRadius: '4px', margin: '5px 0', padding: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }); const header = document.createElement('div'); header.className = 'info-header'; setStyles(header, { fontSize: '16px', fontWeight: 'bold', color: dark ? '#fff' : '#000' }); header.textContent = `Item: ${itemName} (ID: ${itemId})`; container.appendChild(header); const sortControls = document.createElement('div'); sortControls.className = 'sort-controls'; setStyles(sortControls, { display: 'flex', alignItems: 'center', gap: '5px', fontSize: '12px', padding: '5px', backgroundColor: dark ? '#333' : '#eee', borderRadius: '4px' }); const sortLabel = document.createElement('span'); sortLabel.textContent = "Sort by:"; sortControls.appendChild(sortLabel); const sortSelect = document.createElement('select'); setStyles(sortSelect, { padding: '2px', border: dark ? '1px solid #444' : '1px solid #ccc', borderRadius: '2px', backgroundColor: dark ? '#1a1a1a' : '#fff', color: dark ? '#fff' : '#000' }); [{ value: "price", text: "Price" }, { value: "quantity", text: "Quantity" }, { value: "updated", text: "Last Updated" }] .forEach(opt => { const option = document.createElement('option'); option.value = opt.value; option.textContent = opt.text; sortSelect.appendChild(option); }); sortSelect.value = currentSortKey; sortControls.appendChild(sortSelect); const orderToggle = document.createElement('button'); setStyles(orderToggle, getButtonStyle()); orderToggle.textContent = currentSortOrder === "asc" ? "Asc" : "Desc"; sortControls.appendChild(orderToggle); const dollarToggle = document.createElement('button'); setStyles(dollarToggle, getButtonStyle()); // Use the persisted state for button text dollarToggle.textContent = showDollarItems ? "Showing $1 Items" : "Hiding $1 Items"; sortControls.appendChild(dollarToggle); allDollarToggles.push(dollarToggle); dollarToggle.addEventListener('click', () => { showDollarItems = !showDollarItems; // Persist the updated state in localStorage localStorage.setItem("showDollarItems", showDollarItems.toString()); allDollarToggles.forEach(btn => { btn.textContent = showDollarItems ? "Showing $1 Items" : "Hiding $1 Items"; }); if (container.filteredListings) renderCards(container, container.filteredListings); }); container.appendChild(sortControls); const scrollWrapper = document.createElement('div'); setStyles(scrollWrapper, { overflowX: 'auto', overflowY: 'hidden', height: '120px', whiteSpace: 'nowrap', paddingBottom: '3px' }); const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; setStyles(cardContainer, { display: 'flex', flexWrap: 'nowrap', gap: '10px' }); scrollWrapper.appendChild(cardContainer); container.appendChild(scrollWrapper); const poweredBy = document.createElement('div'); setStyles(poweredBy, { fontSize: '10px', textAlign: 'right', marginTop: '6px' }); poweredBy.innerHTML = ` Powered by TornPal & IronNerd `; container.appendChild(poweredBy); sortSelect.addEventListener('change', () => { currentSortKey = sortSelect.value; if (container.filteredListings) renderCards(container, container.filteredListings); }); orderToggle.addEventListener('click', () => { currentSortOrder = currentSortOrder === "asc" ? "desc" : "asc"; orderToggle.textContent = currentSortOrder === "asc" ? "Asc" : "Desc"; if (container.filteredListings) renderCards(container, container.filteredListings); }); return container; } function renderCards(infoContainer, listings) { const filtered = showDollarItems ? listings : listings.filter(l => l.price !== 1); const sorted = filtered.slice().sort((a, b) => { let diff = (currentSortKey === "price") ? a.price - b.price : (currentSortKey === "quantity") ? a.quantity - b.quantity : a.updated - b.updated; return currentSortOrder === "asc" ? diff : -diff; }); const cardContainer = infoContainer.querySelector('.card-container'); cardContainer.innerHTML = ''; sorted.forEach(listing => cardContainer.appendChild(createListingCard(listing))); } function createListingCard(listing) { const dark = isDarkMode(); const card = document.createElement('div'); card.className = 'listing-card'; setStyles(card, { position: 'relative', minWidth: '160px', maxWidth: '240px', display: 'inline-block', verticalAlign: 'top', backgroundColor: dark ? '#1a1a1a' : '#fff', color: dark ? '#fff' : '#000', border: dark ? '1px solid #444' : '1px solid #ccc', borderRadius: '4px', padding: '8px', fontSize: 'clamp(12px, 1vw, 16px)', boxSizing: 'border-box', overflow: 'hidden' }); const linkContainer = document.createElement('div'); setStyles(linkContainer, { display: 'flex', alignItems: 'center', gap: '5px', marginBottom: '6px', flexWrap: 'wrap' }); const visitedKey = `visited_${listing.item_id}_${listing.player_id}`; let visitedData = null; try { visitedData = JSON.parse(localStorage.getItem(visitedKey)); } catch(e){} let linkColor = (visitedData && visitedData.lastClickedUpdated >= listing.updated) ? 'purple' : '#00aaff'; const playerLink = document.createElement('a'); playerLink.href = `https://www.torn.com/bazaar.php?userId=${listing.player_id}&itemId=${listing.item_id}&highlight=1#/`; playerLink.textContent = `Player: ${listing.player_id}`; setStyles(playerLink, { fontWeight: 'bold', color: linkColor, textDecoration: 'underline' }); playerLink.addEventListener('click', () => { localStorage.setItem(visitedKey, JSON.stringify({ lastClickedUpdated: listing.updated })); playerLink.style.color = 'purple'; }); linkContainer.appendChild(playerLink); const iconSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); iconSvg.setAttribute("viewBox", "0 0 512 512"); iconSvg.setAttribute("width", "16"); iconSvg.setAttribute("height", "16"); iconSvg.style.cursor = "pointer"; iconSvg.style.color = dark ? '#ffa500' : '#cc6600'; iconSvg.title = "Open in modal"; iconSvg.innerHTML = ` `; iconSvg.addEventListener('click', e => { e.preventDefault(); localStorage.setItem(visitedKey, JSON.stringify({ lastClickedUpdated: listing.updated })); playerLink.style.color = 'purple'; openModal(`https://www.torn.com/bazaar.php?userId=${listing.player_id}&itemId=${listing.item_id}&highlight=1#/`); }); linkContainer.appendChild(iconSvg); card.appendChild(linkContainer); const details = document.createElement('div'); details.innerHTML = `
Price: $${listing.price.toLocaleString()}
Qty: ${listing.quantity}
`; details.style.marginBottom = '6px'; card.appendChild(details); const footnote = document.createElement('div'); setStyles(footnote, { fontSize: '11px', color: dark ? '#aaa' : '#555', textAlign: 'right' }); footnote.textContent = `Updated: ${getRelativeTime(listing.updated)}`; card.appendChild(footnote); const sourceInfo = document.createElement('div'); setStyles(sourceInfo, { fontSize: '10px', color: dark ? '#aaa' : '#555', textAlign: 'right' }); let sourceDisplay = listing.source === "ironnerd" ? "IronNerd" : (listing.source === "bazaar" ? "TornPal" : listing.source); sourceInfo.textContent = "Source: " + sourceDisplay; card.appendChild(sourceInfo); return card; } function updateInfoContainer(wrapper, itemId, itemName) { let infoContainer = document.querySelector(`#item-info-container[data-itemid="${itemId}"]`); if (!infoContainer) { infoContainer = createInfoContainer(itemName, itemId); wrapper.insertBefore(infoContainer, wrapper.firstChild); } else { const header = infoContainer.querySelector('.info-header'); if (header) header.textContent = `Item: ${itemName} (ID: ${itemId})`; const cardContainer = infoContainer.querySelector('.card-container'); if (cardContainer) cardContainer.innerHTML = ''; } const cachedData = getCache(itemId); if (cachedData) { infoContainer.filteredListings = cachedData.listings; renderCards(infoContainer, cachedData.listings); return; } let listings = [], responsesReceived = 0; function processResponse(newListings) { newListings.forEach(newItem => { let normalized = newItem.user_id !== undefined ? { item_id: newItem.item_id, player_id: newItem.user_id, quantity: newItem.quantity, price: newItem.price, updated: newItem.last_updated, source: "ironnerd" } : newItem; let duplicate = listings.find(item => item.player_id === normalized.player_id && item.price === normalized.price && item.quantity === normalized.quantity ); if (duplicate) { if (duplicate.source !== normalized.source) duplicate.source = "TornPal & IronNerd"; } else { listings.push(normalized); } }); responsesReceived++; if (responsesReceived === 2) { setCache(itemId, { listings }); infoContainer.filteredListings = listings; renderCards(infoContainer, listings); } } fetchJSON(`https://tornpal.com/api/v1/markets/clist/${itemId}`, data => { processResponse(data && data.listings && Array.isArray(data.listings) ? data.listings.filter(l => l.source === "bazaar") : []); }); fetchJSON(`https://www.ironnerd.me/get_bazaar_items/${itemId}`, data => { processResponse(data && data.bazaar_items && Array.isArray(data.bazaar_items) ? data.bazaar_items : []); }); } function processSellerWrapper(wrapper) { if (!wrapper || wrapper.id === 'item-info-container') return; const itemTile = wrapper.previousElementSibling; if (!itemTile) return; const nameEl = itemTile.querySelector('.name___ukdHN'); const btn = itemTile.querySelector('button[aria-controls^="wai-itemInfo-"]'); if (nameEl && btn) { const itemName = nameEl.textContent.trim(); const idParts = btn.getAttribute('aria-controls').split('-'); const itemId = idParts[idParts.length - 1]; updateInfoContainer(wrapper, itemId, itemName); } } function processMobileSellerList() { if (window.innerWidth >= 784) return; const sellerList = document.querySelector('ul.sellerList___e4C9_'); if (!sellerList) { const existing = document.querySelector('#item-info-container'); if (existing) existing.remove(); return; } const headerEl = document.querySelector('.itemsHeader___ZTO9r .title___ruNCT'); const itemName = headerEl ? headerEl.textContent.trim() : "Unknown"; const btn = document.querySelector('.itemsHeader___ZTO9r button[aria-controls^="wai-itemInfo-"]'); let itemId = "unknown"; if (btn) { const parts = btn.getAttribute('aria-controls').split('-'); itemId = parts.length > 2 ? parts[parts.length - 2] : parts[parts.length - 1]; } if (document.querySelector(`#item-info-container[data-itemid="${itemId}"]`)) return; const infoContainer = createInfoContainer(itemName, itemId); sellerList.parentNode.insertBefore(infoContainer, sellerList); updateInfoContainer(infoContainer, itemId, itemName); } function processAllSellerWrappers(root = document.body) { if (window.innerWidth < 784) return; root.querySelectorAll('[class*="sellerListWrapper"]').forEach(wrapper => processSellerWrapper(wrapper)); } processAllSellerWrappers(); processMobileSellerList(); const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (window.innerWidth < 784 && node.matches('ul.sellerList___e4C9_')) { processMobileSellerList(); } else { if (node.matches('[class*="sellerListWrapper"]')) processSellerWrapper(node); processAllSellerWrappers(node); } } }); mutation.removedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.matches('ul.sellerList___e4C9_') && window.innerWidth < 784) { const container = document.querySelector('#item-info-container'); if (container) container.remove(); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); if (window.location.href.includes("bazaar.php")) { function scrollToTargetItem() { const params = new URLSearchParams(window.location.search); const targetItemId = params.get("itemId"), highlightParam = params.get("highlight"); if (!targetItemId || highlightParam !== "1") return; function removeHighlightParam() { params.delete("highlight"); history.replaceState({}, "", window.location.pathname + "?" + params.toString() + window.location.hash); } function showToast(message) { const toast = document.createElement('div'); toast.textContent = message; setStyles(toast, { position: 'fixed', bottom: '20px', left: '50%', transform: 'translateX(-50%)', backgroundColor: 'rgba(0,0,0,0.7)', color: 'white', padding: '10px 20px', borderRadius: '5px', zIndex: '100000', fontSize: '14px' }); document.body.appendChild(toast); setTimeout(() => { toast.remove(); }, 3000); } function findItemCard() { const img = document.querySelector(`img[src*="/images/items/${targetItemId}/"]`); return img ? img.closest('.item___GYCYJ') : null; } const scrollInterval = setInterval(() => { const card = findItemCard(); if (card) { clearInterval(scrollInterval); removeHighlightParam(); card.classList.add("green-outline", "pop-flash"); card.scrollIntoView({ behavior: "smooth", block: "center" }); setTimeout(() => { card.classList.remove("pop-flash"); }, 800); } else { if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) { showToast("Item not found on this page."); removeHighlightParam(); clearInterval(scrollInterval); } else { window.scrollBy({ top: 300, behavior: 'auto' }); } } }, 50); } function waitForItems() { const container = document.querySelector('.ReactVirtualized__Grid__innerScrollContainer'); if (container && container.childElementCount > 0) scrollToTargetItem(); else setTimeout(waitForItems, 500); } waitForItems(); } function dailyCleanup() { const lastCleanup = localStorage.getItem("lastDailyCleanup"), oneDay = 24 * 60 * 60 * 1000, now = Date.now(); if (!lastCleanup || (now - parseInt(lastCleanup, 10)) > oneDay) { const sevenDays = 7 * 24 * 60 * 60 * 1000; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && (key.startsWith("visited_") || key.startsWith("tornBazaarCache_"))) { try { const val = JSON.parse(localStorage.getItem(key)); let ts = null; if (key.startsWith("visited_") && val && val.lastClickedUpdated) { ts = val.lastClickedTime || (localStorage.removeItem(key), null); } else if (key.startsWith("tornBazaarCache_") && val && val.timestamp) { ts = val.timestamp; } if (ts !== null && (now - ts) > sevenDays) localStorage.removeItem(key); } catch(e) { localStorage.removeItem(key); } } } localStorage.setItem("lastDailyCleanup", now.toString()); } } })();