// ==UserScript== // @name Bazaars in Item Market powered by TornPal and IronNerd // @namespace http://tampermonkey.net/ // @version 2.20 // @description Displays bazaar listings with sorting controls via TornPal & IronNerd // @author Weav3r // @match https://www.torn.com/* // @grant GM_xmlhttpRequest // @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, CARD_WIDTH = 180; let currentSortKey = "price", currentSortOrder = "asc", allListings = [], currentDarkMode = document.body.classList.contains('dark-mode'), currentItemName = "", displayMode = "percentage", isMobileView = false; let scriptSettings = { defaultSort: "price", defaultOrder: "asc", apiKey: "", listingFee: parseFloat(localStorage.getItem("bazaarListingFee") || "0"), defaultDisplayMode: "percentage", linkBehavior: localStorage.getItem("bazaarLinkBehavior") || "new_tab" }; function checkMobileView() { isMobileView = window.innerWidth < 784; return isMobileView; } checkMobileView(); window.addEventListener('resize', function() { checkMobileView(); processMobileSellerList(); }); function loadSettings() { try { const saved = localStorage.getItem("bazaarsSettings"); if (saved) { scriptSettings = { ...scriptSettings, ...JSON.parse(saved) }; currentSortKey = scriptSettings.defaultSort; currentSortOrder = scriptSettings.defaultOrder; displayMode = scriptSettings.defaultDisplayMode || "percentage"; } } catch (e) { console.error("Oops, settings failed to load:", e); } } function saveSettings() { try { localStorage.setItem("bazaarsSettings", JSON.stringify(scriptSettings)); localStorage.setItem("bazaarApiKey", scriptSettings.apiKey || ""); localStorage.setItem("bazaarDefaultSort", scriptSettings.defaultSort || "price"); localStorage.setItem("bazaarDefaultOrder", scriptSettings.defaultOrder || "asc"); localStorage.setItem("bazaarListingFee", scriptSettings.listingFee || 0); localStorage.setItem("bazaarDefaultDisplayMode", scriptSettings.defaultDisplayMode || "percentage"); localStorage.setItem("bazaarLinkBehavior", scriptSettings.linkBehavior || "new_tab"); } catch (e) { console.error("Settings save hiccup:", e); } } loadSettings(); const style = document.createElement("style"); style.textContent = ` .bazaar-button { padding: 3px 6px; border: 1px solid #ccc; border-radius: 4px; background-color: #fff; color: #000; cursor: pointer; font-size: 12px; margin-left: 4px; } .dark-mode .bazaar-button { border: 1px solid #444; background-color: #1a1a1a; color: #fff; } .bazaar-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); display: flex; justify-content: center; align-items: center; z-index: 99999; } .bazaar-info-container { font-size: 13px; border-radius: 4px; margin: 5px 0; padding: 10px; display: flex; flex-direction: column; gap: 8px; background-color: #f9f9f9; color: #000; border: 1px solid #ccc; box-sizing: border-box; width: 100%; overflow: hidden; } .dark-mode .bazaar-info-container { background-color: #2f2f2f; color: #ccc; border: 1px solid #444; } .bazaar-info-header { font-size: 16px; font-weight: bold; color: #000; } .dark-mode .bazaar-info-header { color: #fff; } .bazaar-sort-controls { display: flex; align-items: center; gap: 5px; font-size: 12px; padding: 5px; background-color: #eee; border-radius: 4px; border: 1px solid #ccc; } .dark-mode .bazaar-sort-controls { background-color: #333; border: 1px solid #444; } .bazaar-sort-select { padding: 3px 24px 3px 8px; border: 1px solid #ccc; border-radius: 4px; background: #fff url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNiIgdmlld0JveD0iMCAwIDEwIDYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTAgMGw1IDYgNS02eiIgZmlsbD0iIzY2NiIvPjwvc3ZnPg==") no-repeat right 8px center; background-size: 10px 6px; -webkit-appearance: none; -moz-appearance: none; appearance: none; cursor: pointer; } .bazaar-profit-tooltip { position: absolute; background: #fff; color: #333; border: 1px solid #ddd; padding: 8px 12px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.3); z-index: 10000; max-width: 280px; pointer-events: none; } .dark-mode .bazaar-profit-tooltip { background: #333; color: #fff; border: 1px solid #555; } .dark-mode .bazaar-sort-select { border: 1px solid #444; background-color: #1a1a1a; color: #fff; background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNiIgdmlld0JveD0iMCAwIDEwIDYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTAgMGw1IDYgNS02eiIgZmlsbD0iI2NjYyIvPjwvc3ZnPg=="); } .bazaar-sort-select:focus { outline: none; border-color: #0078d7; box-shadow: 0 0 0 1px #0078d7; } .bazaar-min-qty { background-color: #fff; color: #000; font-size: 12px; } .dark-mode .bazaar-min-qty { border: 1px solid #444 !important; background-color: #1a1a1a; color: #fff; } .bazaar-min-qty:focus { outline: none; border-color: #0078d7 !important; box-shadow: 0 0 0 1px #0078d7; } .bazaar-scroll-container { position: relative; display: flex; align-items: stretch; width: 100%; box-sizing: border-box; } .bazaar-scroll-wrapper { flex: 1; overflow-x: auto; overflow-y: hidden; height: 100px; white-space: nowrap; padding-bottom: 3px; border-radius: 4px; border: 1px solid #ccc; margin: 0 auto; max-width: calc(100% - 30px); position: relative; } .dark-mode .bazaar-scroll-wrapper { border: 1px solid #444; } .bazaar-scroll-arrow { display: flex; align-items: center; justify-content: center; width: 12px; flex-shrink: 0; flex-grow: 0; cursor: pointer; background-color: transparent; border: none; opacity: 0.5; transition: opacity 0.2s ease; margin: 0 1px; z-index: 2; position: relative; } .bazaar-scroll-arrow:hover { opacity: 0.9; background-color: transparent; } .dark-mode .bazaar-scroll-arrow { background-color: transparent; border: none; } .bazaar-scroll-arrow svg { width: 18px !important; height: 18px !important; color: #888; } .dark-mode .bazaar-scroll-arrow svg { color: #777; } .bazaar-scroll-arrow.left { padding-left: 10px; margin-left: -10px; } .bazaar-scroll-arrow.right { padding-right: 10px; margin-right: -10px; } .bazaar-scroll-wrapper::-webkit-scrollbar { height: 8px; } .bazaar-scroll-wrapper::-webkit-scrollbar-track { background: #f1f1f1; } .bazaar-scroll-wrapper::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; } .bazaar-scroll-wrapper::-webkit-scrollbar-thumb:hover { background: #555; } .dark-mode .bazaar-scroll-wrapper::-webkit-scrollbar-track { background: #333; } .dark-mode .bazaar-scroll-wrapper::-webkit-scrollbar-thumb { background: #555; } .dark-mode .bazaar-scroll-wrapper::-webkit-scrollbar-thumb:hover { background: #777; } .bazaar-card-container { position: relative; height: 100%; display: flex; align-items: center; } .bazaar-listing-card { position: absolute; min-width: 140px; max-width: 200px; display: flex; flex-direction: column; justify-content: space-between; border-radius: 4px; padding: 8px; font-size: clamp(12px, 1vw, 14px); box-sizing: border-box; overflow: hidden; background-color: #fff; color: #000; border: 1px solid #ccc; top: 50%; transform: translateY(-50%); word-break: break-word; height: auto; } .dark-mode .bazaar-listing-card { background-color: #1a1a1a; color: #fff; border: 1px solid #444; } .bazaar-listing-footnote { font-size: 11px; text-align: right; color: #555; } .dark-mode .bazaar-listing-footnote { color: #aaa; } .bazaar-listing-source { font-size: 10px; text-align: right; color: #555; } .dark-mode .bazaar-listing-source { color: #aaa; } .bazaar-footer-container { display: flex; justify-content: space-between; align-items: center; margin-top: 5px; font-size: 10px; } .bazaar-powered-by span { color: #999; } .dark-mode .bazaar-powered-by span { color: #666; } .bazaar-powered-by a { text-decoration: underline; color: #555; } .dark-mode .bazaar-powered-by a { color: #aaa; } @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; } .bazaar-settings-modal { background-color: #fff; border-radius: 8px; padding: 24px; width: 500px; max-width: 95vw; max-height: 90vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3); position: relative; z-index: 100000; font-family: 'Arial', sans-serif; } .dark-mode .bazaar-settings-modal { background-color: #2a2a2a; color: #e0e0e0; border: 1px solid #444; } .bazaar-settings-title { font-size: 20px; font-weight: bold; margin-bottom: 20px; color: #333; } .dark-mode .bazaar-settings-title { color: #fff; } .bazaar-tabs { display: flex; border-bottom: 1px solid #ddd; margin-bottom: 20px; padding-bottom: 0; flex-wrap: wrap; } .dark-mode .bazaar-tabs { border-bottom: 1px solid #444; } .bazaar-tab { padding: 10px 16px; cursor: pointer; margin-right: 5px; margin-bottom: 5px; border: 1px solid transparent; border-bottom: none; border-radius: 4px 4px 0 0; font-weight: normal; background-color: #f5f5f5; color: #555; position: relative; bottom: -1px; } .dark-mode .bazaar-tab { background-color: #333; color: #ccc; } .bazaar-tab.active { background-color: #fff; color: #333; border-color: #ddd; font-weight: bold; padding-bottom: 11px; } .dark-mode .bazaar-tab.active { background-color: #2a2a2a; color: #fff; border-color: #444; } .bazaar-tab-content { display: none; } .bazaar-tab-content.active { display: block; } .bazaar-settings-group { margin-bottom: 20px; } .bazaar-settings-item { margin-bottom: 18px; } .bazaar-settings-item label { display: block; margin-bottom: 8px; font-weight: bold; font-size: 14px; } .bazaar-settings-item input[type="text"], .bazaar-settings-item select, .bazaar-number-input { width: 100%; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; background-color: #fff; color: #333; max-width: 200px; } .dark-mode .bazaar-settings-item input[type="text"], .dark-mode .bazaar-settings-item select, .dark-mode .bazaar-number-input { border: 1px solid #444; background-color: #222; color: #e0e0e0; } .bazaar-settings-item select { max-width: 200px; } .bazaar-number-input { -moz-appearance: textfield; appearance: textfield; width: 60px !important; } .bazaar-number-input::-webkit-outer-spin-button, .bazaar-number-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .bazaar-api-note { font-size: 12px; margin-top: 6px; color: #666; line-height: 1.4; } .dark-mode .bazaar-api-note { color: #aaa; } .bazaar-script-item { margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid #eee; } .dark-mode .bazaar-script-item { border-bottom: 1px solid #333; } .bazaar-script-item:last-child { border-bottom: none; } .bazaar-script-name { font-weight: bold; font-size: 16px; margin-bottom: 5px; } .bazaar-script-desc { margin-bottom: 8px; line-height: 1.4; color: #555; } .dark-mode .bazaar-script-desc { color: #bbb; } .bazaar-script-link { display: inline-block; margin-top: 5px; color: #2196F3; text-decoration: none; } .bazaar-script-link:hover { text-decoration: underline; } .bazaar-changelog { margin-bottom: 20px; } .bazaar-changelog-version { font-weight: bold; margin-bottom: 8px; font-size: 15px; } .bazaar-changelog-date { font-style: italic; color: #666; font-size: 13px; margin-bottom: 5px; } .dark-mode .bazaar-changelog-date { color: #aaa; } .bazaar-changelog-list { margin-left: 20px; margin-bottom: 15px; } .bazaar-changelog-item { margin-bottom: 5px; line-height: 1.4; } .bazaar-credits { margin-top: 20px; padding-top: 15px; border-top: 1px solid #eee; } .dark-mode .bazaar-credits { border-top: 1px solid #444; } .bazaar-credits h3 { font-size: 16px; margin-bottom: 10px; } .bazaar-credits p { line-height: 1.4; margin-bottom: 8px; } .bazaar-provider { font-weight: bold; } .bazaar-settings-buttons { display: flex; justify-content: flex-end; gap: 12px; margin-top: 30px; } .bazaar-settings-save, .bazaar-settings-cancel { padding: 8px 16px; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; font-weight: bold; } .bazaar-settings-save { background-color: #4CAF50; color: white; } .bazaar-settings-save:hover { background-color: #45a049; } .bazaar-settings-cancel { background-color: #f5f5f5; color: #333; border: 1px solid #ddd; } .dark-mode .bazaar-settings-cancel { background-color: #333; color: #e0e0e0; border: 1px solid #444; } .bazaar-settings-cancel:hover { background-color: #e9e9e9; } .dark-mode .bazaar-settings-cancel:hover { background-color: #444 !important; border-color: #555 !important; } .bazaar-settings-footer { margin-top: 20px; font-size: 12px; color: #777; text-align: center; padding-top: 15px; border-top: 1px solid #eee; } .dark-mode .bazaar-settings-footer { color: #999; border-top: 1px solid #444; } .bazaar-settings-footer a { color: #2196F3; text-decoration: none; } .bazaar-settings-footer a:hover { text-decoration: underline; } @media (max-width: 600px) { .bazaar-settings-modal { padding: 16px; width: 100%; max-width: 100%; border-radius: 0; max-height: 100vh; } .bazaar-settings-title { font-size: 18px; margin-bottom: 16px; } .bazaar-tab { padding: 8px 12px; font-size: 14px; } .bazaar-settings-item label { font-size: 13px; } .bazaar-settings-item input[type="text"], .bazaar-settings-item select, .bazaar-number-input { padding: 6px 10px; font-size: 13px; max-width: 100%; } .bazaar-settings-item { margin-bottom: 14px; } .bazaar-settings-save, .bazaar-settings-cancel { padding: 6px 12px; font-size: 13px; } .bazaar-api-note { font-size: 11px; } .bazaar-settings-buttons { margin-top: 20px; } .bazaar-settings-footer { font-size: 11px; } } `; document.head.appendChild(style); function fetchJSON(url, callback) { let retryCount = 0; const MAX_RETRIES = 2; const TIMEOUT_MS = 10000; const RETRY_DELAY_MS = 2000; function makeRequest(options) { if (typeof GM_xmlhttpRequest !== 'undefined') { return GM_xmlhttpRequest(options); } else if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined') { return GM.xmlHttpRequest(options); } else { console.error('Neither GM_xmlhttpRequest nor GM.xmlHttpRequest are available'); options.onerror && options.onerror(new Error('XMLHttpRequest API not available')); return null; } } function attemptFetch() { let timeoutId = setTimeout(() => { console.warn(`Request to ${url} timed out, ${retryCount < MAX_RETRIES ? 'retrying...' : 'giving up.'}`); if (retryCount < MAX_RETRIES) { retryCount++; setTimeout(attemptFetch, RETRY_DELAY_MS); } else { callback(null); } }, TIMEOUT_MS); makeRequest({ method: 'GET', url, timeout: TIMEOUT_MS, onload: res => { clearTimeout(timeoutId); try { if (res.status >= 200 && res.status < 300) { callback(JSON.parse(res.responseText)); } else { console.warn(`Request to ${url} failed with status ${res.status}`); if (retryCount < MAX_RETRIES) { retryCount++; setTimeout(attemptFetch, RETRY_DELAY_MS); } else { callback(null); } } } catch (e) { console.error(`Error parsing response from ${url}:`, e); callback(null); } }, onerror: (error) => { clearTimeout(timeoutId); console.warn(`Request to ${url} failed:`, error); if (retryCount < MAX_RETRIES) { retryCount++; setTimeout(attemptFetch, RETRY_DELAY_MS); } else { callback(null); } }, ontimeout: () => { clearTimeout(timeoutId); console.warn(`Request to ${url} timed out natively`); if (retryCount < MAX_RETRIES) { retryCount++; setTimeout(attemptFetch, RETRY_DELAY_MS); } else { callback(null); } } }); } attemptFetch(); } let cachedItemsData = null; function getStoredItems() { if (cachedItemsData === null) { try { cachedItemsData = JSON.parse(localStorage.getItem("tornItems") || "{}"); } catch (e) { cachedItemsData = {}; console.error("Stored items got funky:", e); } } return cachedItemsData; } function getCache(itemId) { try { const key = "tornBazaarCache_" + itemId, 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'; } const svgTemplates = { rightArrow: ``, leftArrow: ``, warningIcon: ``, infoIcon: `` }; function renderVirtualCards(infoContainer) { const cardContainer = infoContainer.querySelector('.bazaar-card-container'), scrollWrapper = infoContainer.querySelector('.bazaar-scroll-wrapper'); if (!cardContainer || !scrollWrapper || !infoContainer.isConnected) return; try { const minQtyInput = infoContainer.querySelector('.bazaar-min-qty'); const minQty = minQtyInput && minQtyInput.value ? parseInt(minQtyInput.value, 10) : 0; const filteredListings = minQty > 0 ? allListings.filter(listing => listing.quantity >= minQty) : allListings; if (filteredListings.length === 0 && allListings.length > 0) { cardContainer.innerHTML = ''; const messageContainer = document.createElement('div'); messageContainer.style.cssText = 'display:flex; flex-direction:column; align-items:center; justify-content:center; padding:20px; text-align:center; width:100%; height:70px;'; const iconSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); iconSvg.setAttribute("viewBox", "0 0 512 512"); iconSvg.setAttribute("width", "24"); iconSvg.setAttribute("height", "24"); iconSvg.style.marginBottom = "10px"; iconSvg.innerHTML = svgTemplates.infoIcon; const textDiv = document.createElement('div'); textDiv.textContent = `No listings found with quantity ≥ ${minQty}. Try a lower value.`; messageContainer.appendChild(iconSvg); messageContainer.appendChild(textDiv); cardContainer.appendChild(messageContainer); const countElement = infoContainer.querySelector('.bazaar-listings-count'); if (countElement) { countElement.textContent = `No listings match minimum quantity of ${minQty} (from ${allListings.length} total listings)`; } return; } const scrollLeft = scrollWrapper.scrollLeft, containerWidth = scrollWrapper.clientWidth; const visibleCards = Math.ceil(containerWidth / CARD_WIDTH), buffer = Math.max(2, Math.floor(visibleCards / 3)), totalItems = filteredListings.length; if (infoContainer.lastRenderScrollLeft !== undefined && Math.abs(infoContainer.lastRenderScrollLeft - scrollLeft) < CARD_WIDTH * 0.3) { return; } infoContainer.lastRenderScrollLeft = scrollLeft; if (cardContainer.style.width !== (totalItems * CARD_WIDTH) + "px") { cardContainer.style.width = (totalItems * CARD_WIDTH) + "px"; } let startIndex = Math.max(0, Math.floor(scrollLeft / CARD_WIDTH) - buffer), endIndex = Math.min(totalItems, Math.ceil((scrollLeft + containerWidth) / CARD_WIDTH) + buffer); const indicesToRender = new Set(); for (let i = startIndex; i < endIndex; i++) indicesToRender.add(i); const existingCards = {}; const cardsToRemove = []; Array.from(cardContainer.children).forEach(card => { if (card.className !== 'bazaar-listing-card') return; const index = parseInt(card.dataset.index, 10); if (isNaN(index)) { cardsToRemove.push(card); } else if (!indicesToRender.has(index)) { cardsToRemove.push(card); } else { existingCards[index] = card; indicesToRender.delete(index); } }); cardsToRemove.forEach(card => card.remove()); if (indicesToRender.size > 0) { const fragment = document.createDocumentFragment(); indicesToRender.forEach(index => fragment.appendChild(createListingCard(filteredListings[index], index))); if (fragment.childElementCount > 0) cardContainer.appendChild(fragment); } const totalQuantity = filteredListings.reduce((sum, listing) => sum + listing.quantity, 0); const totalListings = allListings.length; const filteredCount = filteredListings.length; const countElement = infoContainer.querySelector('.bazaar-listings-count'); if (countElement) { if (minQty > 0 && filteredCount < totalListings) { countElement.textContent = `Showing ${filteredCount} of ${totalListings} bazaars (${totalQuantity.toLocaleString()} items total, min qty: ${minQty})`; } else { countElement.textContent = `Showing bazaars ${startIndex + 1}-${endIndex} of ${totalItems} (${totalQuantity.toLocaleString()} items total)`; } } } catch (error) { console.error("Error rendering virtual cards:", error); } } function createInfoContainer(itemName, itemId) { const container = document.createElement('div'); container.className = 'bazaar-info-container'; container.dataset.itemid = itemId; currentItemName = itemName; const header = document.createElement('div'); header.className = 'bazaar-info-header'; let marketValueText = ""; try { const stored = getStoredItems(); const match = Object.values(stored).find(item => item.name && item.name.toLowerCase() === itemName.toLowerCase()); if (match && match.market_value) { marketValueText = `Market Value: $${Number(match.market_value).toLocaleString()}`; } } catch (e) { console.error("Header market value error:", e); } header.textContent = `Bazaar Listings for ${itemName} (ID: ${itemId})`; if (marketValueText) { const span = document.createElement('span'); span.style.marginLeft = '8px'; span.style.fontSize = '14px'; span.style.fontWeight = 'normal'; span.style.color = currentDarkMode ? '#aaa' : '#666'; span.textContent = `• ${marketValueText}`; header.appendChild(span); } container.appendChild(header); currentSortOrder = getSortOrderForKey(currentSortKey); const sortControls = document.createElement('div'); sortControls.className = 'bazaar-sort-controls'; sortControls.innerHTML = ` Sort by: Min Qty: `; container.appendChild(sortControls); const scrollContainer = document.createElement('div'); scrollContainer.className = 'bazaar-scroll-container'; function createScrollArrow(direction) { const arrow = document.createElement('div'); arrow.className = `bazaar-scroll-arrow ${direction}`; arrow.innerHTML = svgTemplates[direction === 'left' ? 'leftArrow' : 'rightArrow']; let isScrolling = false, scrollAnimationId = null, startTime = 0, isClickAction = false; const ACTION_THRESHOLD = 200; function smoothScroll() { if (!isScrolling) return; scrollWrapper.scrollLeft += (direction === 'left' ? -1.5 : 1.5); scrollAnimationId = requestAnimationFrame(smoothScroll); } function startScrolling(e) { e.preventDefault(); startTime = Date.now(); isClickAction = false; setTimeout(() => { if (startTime && Date.now() - startTime >= ACTION_THRESHOLD) { isScrolling = true; smoothScroll(); } }, ACTION_THRESHOLD); } function stopScrolling() { const holdDuration = Date.now() - startTime; isScrolling = false; if (scrollAnimationId) { cancelAnimationFrame(scrollAnimationId); scrollAnimationId = null; } if (holdDuration < ACTION_THRESHOLD && !isClickAction) { isClickAction = true; scrollWrapper.scrollBy({ left: direction === 'left' ? -200 : 200, behavior: 'smooth' }); } startTime = 0; } arrow.addEventListener('mousedown', startScrolling); arrow.addEventListener('mouseup', stopScrolling); arrow.addEventListener('mouseleave', stopScrolling); arrow.addEventListener('touchstart', startScrolling, { passive: false }); arrow.addEventListener('touchend', stopScrolling); arrow.addEventListener('touchcancel', stopScrolling); return arrow; } scrollContainer.appendChild(createScrollArrow('left')); const scrollWrapper = document.createElement('div'); scrollWrapper.className = 'bazaar-scroll-wrapper'; const cardContainer = document.createElement('div'); cardContainer.className = 'bazaar-card-container'; scrollWrapper.appendChild(cardContainer); scrollContainer.appendChild(scrollWrapper); scrollContainer.appendChild(createScrollArrow('right')); scrollWrapper.addEventListener('scroll', () => { if (!scrollWrapper.isScrolling) { scrollWrapper.isScrolling = true; requestAnimationFrame(function checkScroll() { renderVirtualCards(container); if (scrollWrapper.lastKnownScrollLeft === scrollWrapper.scrollLeft) { renderVirtualCards(container); scrollWrapper.isScrolling = false; } else { scrollWrapper.lastKnownScrollLeft = scrollWrapper.scrollLeft; requestAnimationFrame(checkScroll); } }); } }); container.appendChild(scrollContainer); const footerContainer = document.createElement('div'); footerContainer.className = 'bazaar-footer-container'; const listingsCount = document.createElement('div'); listingsCount.className = 'bazaar-listings-count'; listingsCount.textContent = 'Loading...'; footerContainer.appendChild(listingsCount); const poweredBy = document.createElement('div'); poweredBy.className = 'bazaar-powered-by'; poweredBy.innerHTML = ` Powered by TornPal & IronNerd `; footerContainer.appendChild(poweredBy); container.appendChild(footerContainer); return container; } function createListingCard(listing, index) { const card = document.createElement('div'); card.className = 'bazaar-listing-card'; card.dataset.index = index; card.style.position = "absolute"; card.style.left = (index * CARD_WIDTH) + "px"; card.style.width = CARD_WIDTH + "px"; let visitedColor = '#00aaff'; try { const key = `visited_${listing.item_id}_${listing.player_id}`; const data = JSON.parse(localStorage.getItem(key)); if (data && data.lastClickedUpdated >= listing.updated) { visitedColor = 'purple'; } } catch (e) {} const displayName = listing.player_name ? listing.player_name : `ID: ${listing.player_id}`; card.innerHTML = `
Player: ${displayName}
Price: $${listing.price.toLocaleString()}
Qty: ${listing.quantity} ${getPriceComparisonHtml(listing.price, listing.quantity)}
Updated: ${getRelativeTime(listing.updated)}
Source: ${listing.source === "ironnerd" ? "IronNerd" : (listing.source === "bazaar" ? "TornPal" : listing.source)}
`; const playerLink = card.querySelector('a'); playerLink.addEventListener('click', (e) => { localStorage.setItem(playerLink.dataset.visitedKey, JSON.stringify({ lastClickedUpdated: listing.updated })); playerLink.style.color = 'purple'; const behavior = scriptSettings.linkBehavior || 'new_tab'; if (behavior !== 'same_tab') { e.preventDefault(); if (behavior === 'new_window') { window.open(playerLink.href, '_blank', 'noopener,noreferrer,width=1200,height=800'); } else { window.open(playerLink.href, '_blank', 'noopener,noreferrer'); } } }); const priceComparison = card.querySelector('.bazaar-price-comparison'); if (priceComparison) { const tooltip = document.createElement('div'); tooltip.className = 'bazaar-profit-tooltip'; tooltip.style.display = 'none'; tooltip.style.position = 'absolute'; tooltip.style.zIndex = '10000'; tooltip.style.padding = '8px 12px'; tooltip.style.borderRadius = '4px'; tooltip.style.fontSize = '12px'; tooltip.style.boxShadow = '0 2px 8px rgba(0,0,0,0.3)'; tooltip.style.maxWidth = '300px'; tooltip.style.backgroundColor = currentDarkMode ? '#333' : '#fff'; tooltip.style.color = currentDarkMode ? '#fff' : '#333'; tooltip.style.border = currentDarkMode ? '1px solid #555' : '1px solid #ddd'; tooltip.innerHTML = priceComparison.getAttribute('data-tooltip'); priceComparison.addEventListener('mouseenter', e => { document.body.appendChild(tooltip); tooltip.style.display = 'block'; const rect = e.target.getBoundingClientRect(); tooltip.style.left = (rect.left + window.scrollX) + 'px'; tooltip.style.top = (rect.bottom + window.scrollY + 5) + 'px'; const tooltipRect = tooltip.getBoundingClientRect(); if (tooltipRect.right > window.innerWidth) { tooltip.style.left = (window.innerWidth - tooltipRect.width - 10) + 'px'; } }); priceComparison.addEventListener('mouseleave', () => { if (tooltip.parentNode) tooltip.parentNode.removeChild(tooltip); }); } return card; } function getPriceComparisonHtml(listingPrice, quantity) { try { const stored = getStoredItems(); const match = Object.values(stored).find(item => item.name && item.name.toLowerCase() === currentItemName.toLowerCase()); if (match && match.market_value) { const marketValue = Number(match.market_value), priceDiff = listingPrice - marketValue, percentDiff = ((listingPrice / marketValue) - 1) * 100, listingFee = scriptSettings.listingFee || 0, totalCost = listingPrice * quantity, potentialRevenue = marketValue * quantity, feeAmount = Math.ceil(potentialRevenue * (listingFee / 100)), potentialProfit = potentialRevenue - totalCost - feeAmount, minResellPrice = Math.ceil(listingPrice / (1 - (listingFee / 100))); let color, text; const absProfit = Math.abs(potentialProfit); let abbrevValue = potentialProfit < 0 ? '-' : ''; if (absProfit >= 1000000) { abbrevValue += '$' + (absProfit / 1000000).toFixed(1).replace(/\.0$/, '') + 'm'; } else if (absProfit >= 1000) { abbrevValue += '$' + (absProfit / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; } else { abbrevValue += '$' + absProfit; } if (potentialProfit > 0) { color = currentDarkMode ? '#7fff7f' : '#006400'; text = displayMode === "percentage" ? `(${percentDiff.toFixed(1)}%)` : `(${abbrevValue})`; } else if (potentialProfit < 0) { color = currentDarkMode ? '#ff7f7f' : '#8b0000'; text = displayMode === "percentage" ? `(+${percentDiff.toFixed(1)}%)` : `(${abbrevValue})`; } else { color = currentDarkMode ? '#cccccc' : '#666666'; text = displayMode === "percentage" ? `(0%)` : `($0)`; } const tooltipContent = `
${potentialProfit >= 0 ? 'PROFIT' : 'LOSS'}: ${potentialProfit >= 0 ? '$' : '-$'}${Math.abs(potentialProfit).toLocaleString()}

Buy: $${listingPrice.toLocaleString()} × ${quantity} item${quantity > 1 ? 's' : ''}
Market Value: $${marketValue.toLocaleString()}
${listingFee > 0 ? `
Resale Fee: ${listingFee}% ($${feeAmount.toLocaleString()})
` : ''} ${listingFee > 0 ? `
Min. Resell Price: $${minResellPrice.toLocaleString()}
` : ''} `; const span = document.createElement('span'); span.style.fontWeight = 'bold'; span.style.fontSize = '10px'; span.style.padding = '0 4px'; span.style.borderRadius = '2px'; span.style.color = color; span.style.cursor = 'help'; span.style.whiteSpace = 'nowrap'; span.textContent = text; span.className = 'bazaar-price-comparison'; span.setAttribute('data-tooltip', tooltipContent); return span.outerHTML; } } catch (e) { console.error("Price comparison error:", e); } return ''; } function sortListings(listings) { return listings.slice().sort((a, b) => { let diff; if (currentSortKey === "profit") { try { const stored = getStoredItems(); const match = Object.values(stored).find(item => item.name && item.name.toLowerCase() === currentItemName.toLowerCase()); if (match && match.market_value) { const marketValue = Number(match.market_value), fee = scriptSettings.listingFee || 0, aProfit = (marketValue * a.quantity) - (a.price * a.quantity) - Math.ceil((marketValue * a.quantity) * (fee / 100)), bProfit = (marketValue * b.quantity) - (b.price * b.quantity) - Math.ceil((marketValue * b.quantity) * (fee / 100)); diff = aProfit - bProfit; } else { diff = a.price - b.price; } } catch (e) { console.error("Profit sort error:", e); diff = a.price - b.price; } } else { diff = currentSortKey === "price" ? a.price - b.price : currentSortKey === "quantity" ? a.quantity - b.quantity : a.updated - b.updated; } return currentSortOrder === "asc" ? diff : -diff; }); } function updateInfoContainer(wrapper, itemId, itemName) { if (wrapper.hasAttribute('data-has-bazaar-info')) return; let infoContainer = document.querySelector(`.bazaar-info-container[data-itemid="${itemId}"]`); if (!infoContainer) { infoContainer = createInfoContainer(itemName, itemId); wrapper.insertBefore(infoContainer, wrapper.firstChild); wrapper.setAttribute('data-has-bazaar-info', 'true'); } else if (!wrapper.contains(infoContainer)) { infoContainer = createInfoContainer(itemName, itemId); wrapper.insertBefore(infoContainer, wrapper.firstChild); wrapper.setAttribute('data-has-bazaar-info', 'true'); } else { const header = infoContainer.querySelector('.bazaar-info-header'); if (header) { header.textContent = `Bazaar Listings for ${itemName} (ID: ${itemId})`; } } const cardContainer = infoContainer.querySelector('.bazaar-card-container'); const countElement = infoContainer.querySelector('.bazaar-listings-count'); const updateListingsCount = (text) => { if (countElement) { countElement.textContent = text; } }; const showEmptyState = (isError) => { if (cardContainer) { cardContainer.innerHTML = ''; cardContainer.style.width = ''; renderMessageInContainer(cardContainer, isError); } updateListingsCount(isError ? 'API Error - Check back later' : 'No listings available'); }; if (cardContainer) { cardContainer.innerHTML = '
Loading bazaar listings...
'; } const cachedData = getCache(itemId); if (cachedData) { allListings = sortListings(cachedData.listings); if (allListings.length === 0) { showEmptyState(false); } else { renderVirtualCards(infoContainer); } return; } let listings = [], responses = 0, apiErrors = false; let requestTimeout = setTimeout(() => { console.warn('Bazaar listings request timed out'); if (responses < 2) { showEmptyState(true); responses = 2; } }, 15000); function processResponse(newListings, error) { if (error) { apiErrors = true; } if (Array.isArray(newListings)) { newListings.forEach(newItem => { const 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", player_name: newItem.player_name || null } : newItem; const duplicate = listings.find(item => item.player_id === normalized.player_id && item.price === normalized.price && item.quantity === normalized.quantity ); if (duplicate) { duplicate.source = duplicate.source === normalized.source ? duplicate.source : "TornPal & IronNerd"; if (!duplicate.player_name && normalized.player_name) { duplicate.player_name = normalized.player_name; } } else { listings.push(normalized); } }); } responses++; if (responses === 2) { clearTimeout(requestTimeout); setCache(itemId, { listings }); if (listings.length === 0) { showEmptyState(apiErrors); } else { allListings = sortListings(listings); renderVirtualCards(infoContainer); } } } fetchJSON(`https://tornpal.com/api/v1/markets/clist/${itemId}?comment=wBazaarMarket`, data => { processResponse(data && Array.isArray(data.listings) ? data.listings.filter(l => l.source === "bazaar") : [], data === null); }); fetchJSON(`https://www.ironnerd.me/get_bazaar_items/${itemId}?comment=wBazaarMarket`, data => { processResponse(data && Array.isArray(data.bazaar_items) ? data.bazaar_items : [], data === null); }); } function renderMessageInContainer(container, isApiError) { container.innerHTML = ''; const messageContainer = document.createElement('div'); messageContainer.style.cssText = 'display:flex; flex-direction:column; align-items:center; justify-content:center; padding:20px; text-align:center; width:100%; height:70px;'; const iconSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); iconSvg.setAttribute("viewBox", "0 0 512 512"); iconSvg.setAttribute("width", "24"); iconSvg.setAttribute("height", "24"); iconSvg.style.marginBottom = "10px"; const textDiv = document.createElement('div'); if (isApiError) { iconSvg.innerHTML = svgTemplates.warningIcon; textDiv.textContent = "Unable to load bazaar listings. Please try again later."; textDiv.style.cssText = currentDarkMode ? 'color:#ff9999; font-weight:bold;' : 'color:#cc0000; font-weight:bold;'; } else { iconSvg.innerHTML = svgTemplates.infoIcon; textDiv.textContent = "No bazaar listings available for this item."; } messageContainer.appendChild(iconSvg); messageContainer.appendChild(textDiv); container.appendChild(messageContainer); } function processSellerWrapper(wrapper) { if (!wrapper || wrapper.classList.contains('bazaar-info-container') || wrapper.hasAttribute('data-bazaar-processed')) return; const existingContainer = wrapper.querySelector(':scope > .bazaar-info-container'); if (existingContainer) return; const itemTile = wrapper.previousElementSibling; if (!itemTile) return; const nameEl = itemTile.querySelector('.name___ukdHN'), 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]; wrapper.setAttribute('data-bazaar-processed', 'true'); updateInfoContainer(wrapper, itemId, itemName); } } function processMobileSellerList() { if (!checkMobileView()) return; const sellerList = document.querySelector('ul.sellerList___e4C9_, ul[class*="sellerList"]'); if (!sellerList) { const existing = document.querySelector('.bazaar-info-container'); if (existing && !document.contains(existing.parentNode)) { existing.remove(); } return; } if (sellerList.hasAttribute('data-has-bazaar-container')) { return; } const headerEl = document.querySelector('.itemsHeader___ZTO9r .title___ruNCT, [class*="itemsHeader"] [class*="title"]'); const itemName = headerEl ? headerEl.textContent.trim() : "Unknown"; const btn = document.querySelector('.itemsHeader___ZTO9r button[aria-controls^="wai-itemInfo-"], [class*="itemsHeader"] 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]; } const existingContainer = document.querySelector(`.bazaar-info-container[data-itemid="${itemId}"]`); if (existingContainer) { if (existingContainer.parentNode !== sellerList.parentNode || existingContainer.nextSibling !== sellerList) { sellerList.parentNode.insertBefore(existingContainer, sellerList); } return; } const infoContainer = createInfoContainer(itemName, itemId); sellerList.parentNode.insertBefore(infoContainer, sellerList); sellerList.setAttribute('data-has-bazaar-container', 'true'); updateInfoContainer(infoContainer, itemId, itemName); } function processAllSellerWrappers(root = document.body) { if (checkMobileView()) return; const sellerWrappers = root.querySelectorAll('[class*="sellerListWrapper"]'); sellerWrappers.forEach(wrapper => processSellerWrapper(wrapper)); } processAllSellerWrappers(); processMobileSellerList(); const observeTarget = document.querySelector('#root') || document.body; let isProcessing = false; const observer = new MutationObserver(mutations => { if (isProcessing) return; let needsProcessing = false; mutations.forEach(mutation => { const isOurMutation = Array.from(mutation.addedNodes).some(node => node.nodeType === Node.ELEMENT_NODE && (node.classList.contains('bazaar-info-container') || node.querySelector('.bazaar-info-container')) ); if (isOurMutation) return; mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { needsProcessing = true; } }); mutation.removedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && (node.matches('ul.sellerList___e4C9_') || node.matches('ul[class*="sellerList"]')) && checkMobileView()) { const container = document.querySelector('.bazaar-info-container'); if (container) container.remove(); } }); }); if (needsProcessing) { if (observer.processingTimeout) { clearTimeout(observer.processingTimeout); } observer.processingTimeout = setTimeout(() => { try { isProcessing = true; if (checkMobileView()) { processMobileSellerList(); } else { processAllSellerWrappers(); } } finally { isProcessing = false; observer.processingTimeout = null; } }, 100); } }); observer.observe(observeTarget, { childList: true, subtree: true }); const bodyObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.attributeName === 'class') { currentDarkMode = document.body.classList.contains('dark-mode'); } }); }); bodyObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] }); if (window.location.href.includes("bazaar.php")) { function scrollToTargetItem() { const params = new URLSearchParams(window.location.search); const targetItemId = params.get("itemId"), highlight = params.get("highlight"); if (!targetItemId || highlight !== "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; toast.style.cssText = 'position:fixed; bottom:20px; left:50%; transform:translateX(-50%); background-color:rgba(0,0,0,0.7); color:white; padding:10px 20px; border-radius:5px; z-index:100000; font-size: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.lastClickedUpdated; } else if (key.startsWith("tornBazaarCache_") && val && val.timestamp) { ts = val.timestamp; } else { localStorage.removeItem(key); } if (ts !== null && (now - ts) > sevenDays) { localStorage.removeItem(key); } } catch (e) { localStorage.removeItem(key); } } } localStorage.setItem("lastDailyCleanup", now.toString()); } } dailyCleanup(); document.body.addEventListener('click', event => { const container = event.target.closest('.bazaar-info-container'); if (!container) return; if (event.target.matches('.bazaar-order-toggle')) { currentSortOrder = currentSortOrder === "asc" ? "desc" : "asc"; event.target.textContent = currentSortOrder === "asc" ? "Asc" : "Desc"; performSort(container); } if (event.target.matches('.bazaar-display-toggle')) { displayMode = displayMode === "percentage" ? "profit" : "percentage"; event.target.textContent = displayMode === "percentage" ? "%" : "$"; scriptSettings.defaultDisplayMode = displayMode; saveSettings(); const cardContainer = container.querySelector('.bazaar-card-container'); if (cardContainer) { cardContainer.innerHTML = ''; container.lastRenderScrollLeft = undefined; renderVirtualCards(container); const scrollWrapper = container.querySelector('.bazaar-scroll-wrapper'); if (scrollWrapper) { const currentScroll = scrollWrapper.scrollLeft; scrollWrapper.scrollLeft = currentScroll + 1; setTimeout(() => { scrollWrapper.scrollLeft = currentScroll; }, 10); } } } }); document.body.addEventListener('change', event => { const container = event.target.closest('.bazaar-info-container'); if (!container) return; if (event.target.matches('.bazaar-sort-select')) { const newSortKey = event.target.value; if (newSortKey !== currentSortKey) { currentSortKey = newSortKey; currentSortOrder = getSortOrderForKey(currentSortKey); const orderToggle = container.querySelector('.bazaar-order-toggle'); if (orderToggle) { orderToggle.textContent = currentSortOrder === "asc" ? "Asc" : "Desc"; } } else { currentSortKey = newSortKey; } performSort(container); } else if (event.target.matches('.bazaar-min-qty')) { const cardContainer = container.querySelector('.bazaar-card-container'); const scrollWrapper = container.querySelector('.bazaar-scroll-wrapper'); if (cardContainer && scrollWrapper) { scrollWrapper.scrollLeft = 0; container.lastRenderScrollLeft = undefined; cardContainer.innerHTML = ''; cardContainer.style.width = ''; renderVirtualCards(container); } } }); document.body.addEventListener('input', event => { const container = event.target.closest('.bazaar-info-container'); if (!container) return; if (event.target.matches('.bazaar-min-qty')) { const minQty = event.target.value ? parseInt(event.target.value, 10) : 0; clearTimeout(event.target.debounceTimer); event.target.debounceTimer = setTimeout(() => { const cardContainer = container.querySelector('.bazaar-card-container'); const scrollWrapper = container.querySelector('.bazaar-scroll-wrapper'); if (cardContainer && scrollWrapper) { scrollWrapper.scrollLeft = 0; container.lastRenderScrollLeft = undefined; cardContainer.innerHTML = ''; cardContainer.style.width = ''; renderVirtualCards(container); } }, 300); } }); function performSort(container) { allListings = sortListings(allListings); const cardContainer = container.querySelector('.bazaar-card-container'); const scrollWrapper = container.querySelector('.bazaar-scroll-wrapper'); if (cardContainer && scrollWrapper) { cardContainer.innerHTML = ''; scrollWrapper.scrollLeft = 0; container.lastRenderScrollLeft = undefined; cardContainer.style.width = ''; renderVirtualCards(container); } } function addSettingsMenuItem() { const menu = document.querySelector('.settings-menu'); if (!menu || document.querySelector('.bazaar-settings-button')) return; const li = document.createElement('li'); li.className = 'link bazaar-settings-button'; const a = document.createElement('a'); a.href = '#'; const iconDiv = document.createElement('div'); iconDiv.className = 'icon-wrapper'; const svgIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svgIcon.setAttribute('class', 'default'); svgIcon.setAttribute('fill', '#fff'); svgIcon.setAttribute('stroke', 'transparent'); svgIcon.setAttribute('stroke-width', '0'); svgIcon.setAttribute('width', '16'); svgIcon.setAttribute('height', '16'); svgIcon.setAttribute('viewBox', '0 0 640 512'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'M36.8 192l566.3 0c20.3 0 36.8-16.5 36.8-36.8c0-7.3-2.2-14.4-6.2-20.4L558.2 21.4C549.3 8 534.4 0 518.3 0L121.7 0c-16 0-31 8-39.9 21.4L6.2 134.7c-4 6.1-6.2 13.2-6.2 20.4C0 175.5 16.5 192 36.8 192zM64 224l0 160 0 80c0 26.5 21.5 48 48 48l224 0c26.5 0 48-21.5 48-48l0-80 0-160-64 0 0 160-192 0 0-160-64 0zm448 0l0 256c0 17.7 14.3 32 32 32s32-14.3 32-32l0-256-64 0z'); const span = document.createElement('span'); span.textContent = 'Bazaar Settings'; svgIcon.appendChild(path); iconDiv.appendChild(svgIcon); a.appendChild(iconDiv); a.appendChild(span); li.appendChild(a); a.addEventListener('click', e => { e.preventDefault(); document.body.click(); openSettingsModal(); }); const logoutButton = menu.querySelector('li.logout'); if (logoutButton) { menu.insertBefore(li, logoutButton); } else { menu.appendChild(li); } } function openSettingsModal() { const overlay = document.createElement("div"); overlay.className = "bazaar-modal-overlay"; const modal = document.createElement("div"); modal.className = "bazaar-settings-modal"; modal.innerHTML = `
Bazaar Listings Settings
Settings
Other Scripts
Providing an API key enables market value comparison. Your key stays local.
Alternatively, install Bazaar Filler, which works seamlessly with this script (Only ONE API call is made each day!)
Choose how listings are sorted: Price, Quantity, Profit, or Last Updated.
Choose the sorting direction.
Set the fee percentage when listing items. (e.g., 10% fee means $10,000 on $100,000)
Choose whether to display price comparisons as a percentage or in dollars.
Choose how bazaar links open when clicked.
Customizable Bazaar Filler
Auto-fills bazaar item quantities and prices.
Install from Greasy Fork
Torn Item Market Highlighter
Highlights items based on rules and prices.
Install from Greasy Fork
Torn Item Market Max Quantity Calculator
Calculates the max quantity you can buy.
Install from Greasy Fork
Enhanced Chat Buttons V2
Improves chat with extra buttons.
Install from Greasy Fork
Market Item Locker
Lock items when listing to avoid accidental sales.
Install from Greasy Fork
Market Quick Remove
Quickly remove items from your listings.
Install from Greasy Fork
Trade Chat Timer on Button
Adds a timer to the trade chat button.
Install from Greasy Fork
`; overlay.appendChild(modal); const tabs = modal.querySelectorAll('.bazaar-tab'); tabs.forEach(tab => { tab.addEventListener('click', function () { tabs.forEach(t => t.classList.remove('active')); this.classList.add('active'); modal.querySelectorAll('.bazaar-tab-content').forEach(content => content.classList.remove('active')); document.getElementById(`tab-${this.getAttribute('data-tab')}`).classList.add('active'); }); }); modal.querySelector('.bazaar-settings-save').addEventListener('click', () => { saveSettingsFromModal(modal); overlay.remove(); }); modal.querySelector('.bazaar-settings-cancel').addEventListener('click', () => { overlay.remove(); }); overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); }); document.body.appendChild(overlay); } function saveSettingsFromModal(modal) { const oldLinkBehavior = scriptSettings.linkBehavior; scriptSettings = { apiKey: modal.querySelector('#bazaar-api-key').value.trim(), defaultSort: modal.querySelector('#bazaar-default-sort').value, defaultOrder: modal.querySelector('#bazaar-default-order').value, listingFee: Math.round(parseFloat(modal.querySelector('#bazaar-listing-fee').value) || 0), defaultDisplayMode: modal.querySelector('#bazaar-default-display').value, linkBehavior: modal.querySelector('#bazaar-link-behavior').value }; if (scriptSettings.listingFee < 0) scriptSettings.listingFee = 0; if (scriptSettings.listingFee > 100) scriptSettings.listingFee = 100; currentSortKey = scriptSettings.defaultSort; currentSortOrder = scriptSettings.defaultOrder; displayMode = scriptSettings.defaultDisplayMode; saveSettings(); document.querySelectorAll('.bazaar-info-container').forEach(container => { const sortSelect = container.querySelector('.bazaar-sort-select'); if (sortSelect) sortSelect.value = currentSortKey; const orderToggle = container.querySelector('.bazaar-order-toggle'); if (orderToggle) orderToggle.textContent = currentSortOrder === "asc" ? "Asc" : "Desc"; const displayToggle = container.querySelector('.bazaar-display-toggle'); if (displayToggle) displayToggle.textContent = displayMode === "percentage" ? "%" : "$"; if (oldLinkBehavior !== scriptSettings.linkBehavior) { const cardContainer = container.querySelector('.bazaar-card-container'); if (cardContainer) { cardContainer.innerHTML = ''; container.lastRenderScrollLeft = undefined; renderVirtualCards(container); } } else { performSort(container); } }); if (scriptSettings.apiKey) { fetchTornItems(); } } function fetchTornItems() { const stored = localStorage.getItem("tornItems"), lastUpdated = localStorage.getItem("lastTornItemsUpdate") || 0, now = Date.now(), oneDayMs = 24 * 60 * 60 * 1000, lastUTC = new Date(parseInt(lastUpdated)).toISOString().split('T')[0], todayUTC = new Date().toISOString().split('T')[0]; if (scriptSettings.apiKey && (!stored || lastUTC < todayUTC || (now - lastUpdated) >= oneDayMs)) { fetch(`https://api.torn.com/torn/?key=${scriptSettings.apiKey}&selections=items&comment=wBazaars`) .then(r => r.json()) .then(data => { if (!data.items) { console.error("Failed to fetch Torn items. Check your API key or rate limit."); return; } const filtered = {}; for (let [id, item] of Object.entries(data.items)) { if (item.tradeable) { filtered[id] = { name: item.name, market_value: item.market_value }; } } localStorage.setItem("tornItems", JSON.stringify(filtered)); localStorage.setItem("lastTornItemsUpdate", now.toString()); }) .catch(err => console.error("Error fetching Torn items:", err)); } } function observeUserMenu() { const menuObserver = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('settings-menu')) { addSettingsMenuItem(); break; } } } }); }); menuObserver.observe(document.body, { childList: true, subtree: true }); if (document.querySelector('.settings-menu')) { addSettingsMenuItem(); } } function getSortOrderForKey(key) { return key === "price" ? "asc" : "desc"; } observeUserMenu(); function cleanupResources() { if (observer) { observer.disconnect(); } if (bodyObserver) { bodyObserver.disconnect(); } document.querySelectorAll('.bazaar-scroll-container').forEach(container => { const scrollWrapper = container.querySelector('.bazaar-scroll-wrapper'); if (scrollWrapper && scrollWrapper.isScrolling) { cancelAnimationFrame(scrollWrapper.scrollAnimationId); } }); } window.addEventListener('beforeunload', cleanupResources); })();