// ==UserScript== // @name Bazaars in Item Market 2.0 // @namespace http://tampermonkey.net/ // @version 0.1 // @description Displays bazaar listings with sorting controls via TornPal // @author Weav3r [1853324] // @match https://www.torn.com/page.php?sid=ItemMarket* // @grant GM_xmlhttpRequest // @connect tornpal.com // @run-at document-end // @downloadURL none // ==/UserScript== (function() { 'use strict'; // Cache duration: 60 seconds const CACHE_DURATION_MS = 60000; // Global sort settings let currentSortKey = "price"; // "price", "quantity", or "updated" let currentSortOrder = "asc"; // "asc" or "desc" // Helpers: caching 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) { // intentionally left blank } } // Helper: relative time 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'; } // Creates the info container function createInfoContainer(itemName, itemId) { const container = document.createElement('div'); container.id = 'item-info-container'; container.setAttribute('data-itemid', itemId); container.style.backgroundColor = '#2f2f2f'; container.style.color = '#ccc'; container.style.fontSize = '13px'; container.style.border = '1px solid #444'; container.style.borderRadius = '4px'; container.style.margin = '5px 0'; container.style.padding = '10px'; container.style.display = 'flex'; container.style.flexDirection = 'column'; container.style.gap = '8px'; // Header const header = document.createElement('div'); header.className = 'info-header'; header.style.fontSize = '16px'; header.style.fontWeight = 'bold'; header.style.color = '#fff'; header.textContent = `Item: ${itemName} (ID: ${itemId})`; container.appendChild(header); // Sort controls const sortControls = document.createElement('div'); sortControls.className = 'sort-controls'; sortControls.style.display = 'flex'; sortControls.style.alignItems = 'center'; sortControls.style.gap = '5px'; sortControls.style.fontSize = '12px'; sortControls.style.padding = '5px'; sortControls.style.backgroundColor = '#333'; sortControls.style.borderRadius = '4px'; const sortLabel = document.createElement('span'); sortLabel.textContent = "Sort by:"; sortControls.appendChild(sortLabel); const sortSelect = document.createElement('select'); sortSelect.style.padding = '2px'; sortSelect.style.border = '1px solid #444'; sortSelect.style.borderRadius = '2px'; sortSelect.style.backgroundColor = '#1a1a1a'; sortSelect.style.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'); orderToggle.style.padding = '2px 4px'; orderToggle.style.border = '1px solid #444'; orderToggle.style.borderRadius = '2px'; orderToggle.style.backgroundColor = '#1a1a1a'; orderToggle.style.color = '#fff'; orderToggle.style.cursor = 'pointer'; orderToggle.textContent = (currentSortOrder === "asc") ? "Asc" : "Desc"; sortControls.appendChild(orderToggle); container.appendChild(sortControls); // Scrollable listings row const scrollWrapper = document.createElement('div'); scrollWrapper.style.overflowX = 'auto'; scrollWrapper.style.overflowY = 'hidden'; scrollWrapper.style.height = '120px'; scrollWrapper.style.whiteSpace = 'nowrap'; scrollWrapper.style.paddingBottom = '3px'; const cardContainer = document.createElement('div'); cardContainer.className = 'card-container'; cardContainer.style.display = 'inline-flex'; cardContainer.style.flexWrap = 'nowrap'; cardContainer.style.gap = '10px'; scrollWrapper.appendChild(cardContainer); container.appendChild(scrollWrapper); // Sorting events 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; } // Sort + render listing cards 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); }); } // Create listing card function createListingCard(listing) { const card = document.createElement('div'); card.className = 'listing-card'; card.style.backgroundColor = '#1a1a1a'; card.style.color = '#fff'; card.style.border = '1px solid #444'; card.style.borderRadius = '4px'; card.style.padding = '8px'; card.style.minWidth = '200px'; card.style.fontSize = '14px'; card.style.display = 'inline-block'; card.style.boxSizing = 'border-box'; const playerLink = document.createElement('a'); playerLink.href = `https://www.torn.com/bazaar.php?userId=${listing.player_id}#/`; playerLink.target = '_blank'; playerLink.textContent = `Player: ${listing.player_id}`; playerLink.style.display = 'block'; playerLink.style.fontWeight = 'bold'; playerLink.style.color = '#00aaff'; playerLink.style.textDecoration = 'underline'; playerLink.style.marginBottom = '6px'; const details = document.createElement('div'); details.innerHTML = `
Price: $${listing.price.toLocaleString()}
Qty: ${listing.quantity}
`; details.style.marginBottom = '6px'; const footnote = document.createElement('div'); footnote.style.fontSize = '11px'; footnote.style.color = '#aaa'; footnote.style.textAlign = 'right'; footnote.textContent = `Updated: ${getRelativeTime(listing.updated)}`; card.appendChild(playerLink); card.appendChild(details); card.appendChild(footnote); return card; } // Fetch data + update container function updateInfoContainer(wrapper, itemId, itemName) { // Check globally for an existing container let infoContainer = document.querySelector(`#item-info-container[data-itemid="${itemId}"]`); if (!infoContainer) { infoContainer = createInfoContainer(itemName, itemId); wrapper.insertBefore(infoContainer, wrapper.firstChild); } else { // Update header if container exists 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; } const url = `https://tornpal.com/api/v1/markets/clist/${itemId}`; GM_xmlhttpRequest({ method: 'GET', url: url, 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"); setCache(itemId, { listings: filtered }); infoContainer.filteredListings = filtered; renderCards(infoContainer, filtered); } else { } } catch (e) { } }, onerror: function(err) { } }); } // Desktop: process each sellerListWrapper 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); } } // Mobile: handle the seller list function processMobileSellerList() { if (window.innerWidth >= 784) return; // only mobile const sellerList = document.querySelector('ul.sellerList___e4C9_'); // If no seller rows, remove container if present if (!sellerList) { const existing = document.querySelector('#item-info-container'); if (existing) existing.remove(); return; } // If we already created a container for this item, skip 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 container for this item ID exists, skip if (document.querySelector(`#item-info-container[data-itemid="${itemId}"]`)) return; // Create + fetch const infoContainer = createInfoContainer(itemName, itemId); sellerList.parentNode.insertBefore(infoContainer, sellerList); updateInfoContainer(infoContainer, itemId, itemName); } // Desktop aggregator function processAllSellerWrappers(root = document.body) { // Skip on mobile if (window.innerWidth < 784) return; const wrappers = root.querySelectorAll('[class*="sellerListWrapper"]'); wrappers.forEach(wrapper => processSellerWrapper(wrapper)); } processAllSellerWrappers(); processMobileSellerList(); // Observe changes (both added and removed nodes) const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { // Handle added nodes 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); } } }); // Handle removed nodes (remove container if sellerList goes away on mobile) 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 }); })();