// ==UserScript== // @name Bazaars in Item Market 2.0 // @namespace http://tampermonkey.net/ // @version 0.5 // @description Displays bazaar listings with sorting controls via TornPal & IronNerd // @author Weav3r [1853324] // @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'; const CACHE_DURATION_MS = 60000; let currentSortKey = "price"; let currentSortOrder = "asc"; 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 { const key = "tornBazaarCache_" + itemId; const payload = { timestamp: Date.now(), data: data }; localStorage.setItem(key, JSON.stringify(payload)); } catch(e) {} } function getRelativeTime(timestampSeconds) { const now = Date.now(); const diffSec = Math.floor((now - timestampSeconds * 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 originalBodyOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; const modalOverlay = document.createElement('div'); modalOverlay.id = 'bazaar-modal-overlay'; Object.assign(modalOverlay.style, { 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'; Object.assign(modalContainer.style, { 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 = '80%'; modalContainer.style.height = '80%'; } } else { modalContainer.style.width = '80%'; modalContainer.style.height = '80%'; } const closeButton = document.createElement('button'); closeButton.textContent = '×'; Object.assign(closeButton.style, { position: 'absolute', top: '-20px', right: '-20px', width: '40px', height: '40px', backgroundColor: '#ff0000', color: '#fff', border: 'none', borderRadius: '50%', fontSize: '24px', cursor: 'pointer', boxShadow: '0 0 5px rgba(0,0,0,0.5)' }); closeButton.addEventListener('click', () => { modalOverlay.remove(); document.body.style.overflow = originalBodyOverflow; }); modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) { modalOverlay.remove(); document.body.style.overflow = originalBodyOverflow; } }); const iframe = document.createElement('iframe'); Object.assign(iframe.style, { width: '100%', height: '100%', border: 'none' }); iframe.src = url; modalContainer.appendChild(closeButton); modalContainer.appendChild(iframe); modalOverlay.appendChild(modalContainer); document.body.appendChild(modalOverlay); if (window.ResizeObserver) { const resizeObserver = 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) })); } }); resizeObserver.observe(modalContainer); } } function createInfoContainer(itemName, itemId) { const container = document.createElement('div'); container.id = 'item-info-container'; container.setAttribute('data-itemid', itemId); Object.assign(container.style, { backgroundColor: '#2f2f2f', color: '#ccc', fontSize: '13px', border: '1px solid #444', borderRadius: '4px', margin: '5px 0', padding: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }); const header = document.createElement('div'); header.className = 'info-header'; Object.assign(header.style, { fontSize: '16px', fontWeight: 'bold', color: '#fff' }); header.textContent = `Item: ${itemName} (ID: ${itemId})`; container.appendChild(header); const sortControls = document.createElement('div'); sortControls.className = 'sort-controls'; Object.assign(sortControls.style, { display: 'flex', alignItems: 'center', gap: '5px', fontSize: '12px', padding: '5px', backgroundColor: '#333', borderRadius: '4px' }); const sortLabel = document.createElement('span'); sortLabel.textContent = "Sort by:"; sortControls.appendChild(sortLabel); const sortSelect = document.createElement('select'); Object.assign(sortSelect.style, { padding: '2px', border: '1px solid #444', borderRadius: '2px', backgroundColor: '#1a1a1a', color: '#fff' }); [ { 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'); Object.assign(orderToggle.style, { padding: '2px 4px', border: '1px solid #444', borderRadius: '2px', backgroundColor: '#1a1a1a', color: '#fff', cursor: 'pointer' }); orderToggle.textContent = (currentSortOrder === "asc") ? "Asc" : "Desc"; sortControls.appendChild(orderToggle); container.appendChild(sortControls); // Listings row const scrollWrapper = document.createElement('div'); Object.assign(scrollWrapper.style, { overflowX: 'auto', overflowY: 'hidden', height: '120px', whiteSpace: 'nowrap', paddingBottom: '3px' }); const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; Object.assign(cardContainer.style, { display: 'flex', flexWrap: 'nowrap', gap: '10px' }); scrollWrapper.appendChild(cardContainer); container.appendChild(scrollWrapper); const poweredBy = document.createElement('div'); poweredBy.style.fontSize = '10px'; poweredBy.style.textAlign = 'right'; poweredBy.style.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 sorted = listings.slice().sort((a, b) => { let diff = 0; if (currentSortKey === "price") diff = a.price - b.price; else if (currentSortKey === "quantity") diff = a.quantity - b.quantity; else if (currentSortKey === "updated") diff = a.updated - b.updated; return (currentSortOrder === "asc") ? diff : -diff; }); const cardContainer = infoContainer.querySelector('.card-container'); cardContainer.innerHTML = ''; sorted.forEach(listing => { const card = createListingCard(listing); cardContainer.appendChild(card); }); } function createListingCard(listing) { const card = document.createElement('div'); card.className = 'listing-card'; Object.assign(card.style, { backgroundColor: '#1a1a1a', color: '#fff', border: '1px solid #444', borderRadius: '4px', padding: '8px', width: 'calc((100% - 20px) / 3)', fontSize: 'clamp(12px, 1vw, 16px)', boxSizing: 'border-box' }); const linkContainer = document.createElement('div'); Object.assign(linkContainer.style, { display: 'flex', alignItems: 'center', gap: '5px', marginBottom: '6px' }); const playerLink = document.createElement('a'); playerLink.href = `https://www.torn.com/bazaar.php?userId=${listing.player_id}#/`; playerLink.textContent = `Player: ${listing.player_id}`; Object.assign(playerLink.style, { fontWeight: 'bold', color: '#00aaff', textDecoration: 'underline' }); 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 = "#ffa500"; iconSvg.title = "Open in modal"; iconSvg.innerHTML = ` `; iconSvg.addEventListener('click', (e) => { e.preventDefault(); openModal(playerLink.href); }); linkContainer.appendChild(iconSvg); const details = document.createElement('div'); details.innerHTML = `
Price: $${listing.price.toLocaleString()}
Qty: ${listing.quantity}
`; details.style.marginBottom = '6px'; const footnote = document.createElement('div'); Object.assign(footnote.style, { fontSize: '11px', color: '#aaa', textAlign: 'right' }); footnote.textContent = `Updated: ${getRelativeTime(listing.updated)}`; const sourceInfo = document.createElement('div'); sourceInfo.style.fontSize = '10px'; sourceInfo.style.color = '#aaa'; sourceInfo.style.textAlign = 'right'; let sourceDisplay = (listing.source === "ironnerd") ? "IronNerd" : (listing.source === "bazaar") ? "TornPal" : listing.source; sourceInfo.textContent = "Source: " + sourceDisplay; card.appendChild(linkContainer); card.appendChild(details); card.appendChild(footnote); 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 = []; let responsesReceived = 0; function processResponse(newListings) { newListings.forEach(newItem => { let normalized; if (newItem.user_id !== undefined) { normalized = { item_id: newItem.item_id, player_id: newItem.user_id, quantity: newItem.quantity, price: newItem.price, updated: newItem.last_updated, source: "ironnerd" }; } else { normalized = 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: listings }); infoContainer.filteredListings = listings; renderCards(infoContainer, listings); } } GM_xmlhttpRequest({ method: 'GET', url: `https://tornpal.com/api/v1/markets/clist/${itemId}`, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.listings && Array.isArray(data.listings)) { const filtered = data.listings.filter(l => l.source === "bazaar"); processResponse(filtered); } else { processResponse([]); } } catch (e) { processResponse([]); } }, onerror: function() { processResponse([]); } }); GM_xmlhttpRequest({ method: 'GET', url: `https://www.ironnerd.me/get_bazaar_items/${itemId}`, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data.bazaar_items && Array.isArray(data.bazaar_items)) { processResponse(data.bazaar_items); } else { processResponse([]); } } catch (e) { processResponse([]); } }, onerror: function() { processResponse([]); } }); } 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; const wrappers = root.querySelectorAll('[class*="sellerListWrapper"]'); wrappers.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) { if (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_')) { if (window.innerWidth < 784) { const container = document.querySelector('#item-info-container'); if (container) container.remove(); } } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })();