Displays bazaar listings with sorting controls via TornPal
当前为
// ==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
// ==/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 = `
<div><strong>Price:</strong> $${listing.price.toLocaleString()}</div>
<div><strong>Qty:</strong> ${listing.quantity}</div>
`;
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 });
})();