// ==UserScript==
// @name Bazaars in Item Market 2.0, powered by TornPal & IronNerd
// @namespace http://tampermonkey.net/
// @version 1.21
// @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 = `