// ==UserScript==
// @name PDA Bazaar Listing on Market(TE Market Value, No API Key) - Formatted Prices
// @namespace https://weav3r.dev/
// @version 2.3.6
// @description Bazaar listings on Item Page - Works Mobile/PDA
// @author WTV [3281931] - Mobile Compatible Version
// @match https://www.torn.com/*
// @match https://m.torn.com/*
// @grant GM_xmlhttpRequest
// ==/UserScript==
;(() => {
let currentSortKey = "price"
let currentSortOrder = "asc"
let allListings = []
const filteredListings = []
let currentDarkMode = document.body.classList.contains("dark-mode")
const currentItemName = ""
const displayMode = "percentage"
let isMobileView = false
window._visitedBazaars = new Set()
const scriptSettings = {
defaultSort: "price",
defaultOrder: "asc",
apiKey: "",
listingFee: 0,
defaultDisplayMode: "percentage",
linkBehavior: "new_tab",
layoutMode: "default",
}
function checkMobileView() {
isMobileView = window.innerWidth < 784
return isMobileView
}
checkMobileView()
window.addEventListener("resize", () => {
checkMobileView()
processAllSellerWrappers()
})
const updateStyles = () => {
let styleEl = document.getElementById("bazaar-enhanced-styles")
if (!styleEl) {
styleEl = document.createElement("style")
styleEl.id = "bazaar-enhanced-styles"
document.head.appendChild(styleEl)
}
styleEl.textContent = `
.bazaar-info-container {
font-size: 15px;
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-controls {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
padding: 8px;
background-color: #eee;
border-radius: 4px;
border: 1px solid #ccc;
flex-wrap: wrap;
}
.dark-mode .bazaar-controls {
background-color: #333;
border: 1px solid #444;
}
.bazaar-controls select, .bazaar-controls input, .bazaar-controls button {
padding: 4px 6px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 11px;
background: #fff;
color: #000;
}
.dark-mode .bazaar-controls select,
.dark-mode .bazaar-controls input,
.dark-mode .bazaar-controls button {
background: #444;
color: #fff;
border-color: #666;
}
.bazaar-controls input {
width: 70px;
}
.bazaar-card-container {
display: flex;
overflow-x: auto;
gap: 6px;
padding: 5px;
min-height: 60px;
}
.bazaar-card {
flex: 0 0 auto;
min-width: 140px;
max-width: 140px;
height: 65px;
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: transform 0.2s;
position: relative;
font-size: 15px;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.dark-mode .bazaar-card {
background: #444;
border-color: #666;
color: #fff;
}
.bazaar-card:hover {
transform: scale(1.02);
}
.bazaar-percentage {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
font-weight: bold;
font-size: 14px;
}
@media (max-width: 784px) {
.bazaar-card {
min-width: 120px;
max-width: 120px;
height: 55px;
padding: 4px;
font-size: 14px;
}
.bazaar-controls {
font-size: 10px;
gap: 3px;
padding: 6px;
}
.bazaar-controls input {
width: 60px;
}
.bazaar-percentage {
right: 4px;
font-size: 13px;
}
}
`
}
updateStyles()
const darkModeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === "class") {
const newDarkMode = document.body.classList.contains("dark-mode")
if (newDarkMode !== currentDarkMode) {
currentDarkMode = newDarkMode
updateStyles()
}
}
})
})
darkModeObserver.observe(document.body, { attributes: true })
function sortListings(listings) {
const key = currentSortKey,
order = currentSortOrder
return [...listings].sort((a, b) => {
let valA = a[key] || 0,
valB = b[key] || 0
if (key === "player_name") {
valA = valA.toLowerCase()
valB = valB.toLowerCase()
}
return (valA > valB ? 1 : valA < valB ? -1 : 0) * (order === "asc" ? 1 : -1)
})
}
function applyFilters(listings, filters) {
return listings.filter((l) => {
if (filters.search && !l.player_name.toLowerCase().includes(filters.search.toLowerCase())) return false
if (filters.minPrice && l.price < Number.parseFloat(filters.minPrice)) return false
if (filters.maxPrice && l.price > Number.parseFloat(filters.maxPrice)) return false
if (filters.minQty && l.quantity < Number.parseInt(filters.minQty)) return false
if (filters.maxQty && l.quantity > Number.parseInt(filters.maxQty)) return false
return true
})
}
function renderMessage(container, isError) {
const cardContainer = container.querySelector(".bazaar-card-container")
if (!cardContainer) return
cardContainer.innerHTML = ""
const msg = document.createElement("div")
msg.style.cssText = "color:#666;text-align:center;padding:20px;"
msg.innerHTML = isError
? "API Error<br><span style='font-size:12px;'>Please try again later</span>"
: "No bazaar listings available for this item."
cardContainer.appendChild(msg)
}
function createInfoContainer(itemName, itemId) {
const container = document.createElement("div")
container.className = "bazaar-info-container"
container.dataset.itemid = itemId
container.innerHTML = `
<div class="bazaar-info-header">Bazaar Listings for ${itemName}</div>
<div class="market-value-display" style="font-weight: normal; color: #FFD700; font-size: 14px; margin-top: 4px;"></div>
<div class="bazaar-controls"></div>
<div class="bazaar-card-container"></div>
`
return container
}
function createFilters(container) {
const controls = container.querySelector(".bazaar-controls")
if (!controls) return
controls.innerHTML = ""
const sortSelect = document.createElement("select")
const sortOptions = [
{ value: "price", text: "Price" },
{ value: "quantity", text: "Quantity" },
{ value: "player_name", text: "Player" },
]
sortOptions.forEach((opt) => {
const option = document.createElement("option")
option.value = opt.value
option.textContent = opt.text
if (opt.value === currentSortKey) option.selected = true
sortSelect.appendChild(option)
})
sortSelect.addEventListener("change", () => {
currentSortKey = sortSelect.value
renderCards(container)
})
const orderBtn = document.createElement("button")
orderBtn.textContent = currentSortOrder === "asc" ? "Asc" : "Desc"
orderBtn.addEventListener("click", () => {
currentSortOrder = currentSortOrder === "asc" ? "desc" : "asc"
orderBtn.textContent = currentSortOrder === "asc" ? "Asc" : "Desc"
renderCards(container)
})
const minPrice = document.createElement("input")
minPrice.type = "number"
minPrice.placeholder = "Min Price"
const maxPrice = document.createElement("input")
maxPrice.type = "number"
maxPrice.placeholder = "Max Price"
const minQty = document.createElement("input")
minQty.type = "number"
minQty.placeholder = "Min Qty"
const maxQty = document.createElement("input")
maxQty.type = "number"
maxQty.placeholder = "Max Qty"
const applyBtn = document.createElement("button")
applyBtn.textContent = "Apply"
applyBtn.addEventListener("click", () => renderCards(container))
;[minPrice, maxPrice, minQty, maxQty].forEach((input) => {
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
renderCards(container)
}
})
})
controls.appendChild(sortSelect)
controls.appendChild(orderBtn)
controls.appendChild(minPrice)
controls.appendChild(maxPrice)
controls.appendChild(minQty)
controls.appendChild(maxQty)
controls.appendChild(applyBtn)
}
function processMobileSellerList() {
if (!checkMobileView()) return
const existingContainers = document.querySelectorAll(".bazaar-info-container")
if (existingContainers.length > 0) {
console.log("[v0] Container already exists globally, skipping")
return
}
const sellerList = document.querySelector('ul.sellerList__e4C9, ul[class*="sellerList"]')
if (!sellerList) {
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 infoContainer = createInfoContainer(itemName, itemId)
sellerList.parentNode.insertBefore(infoContainer, sellerList)
sellerList.setAttribute("data-has-bazaar-container", "true")
createFilters(infoContainer)
fetchMarketValueAndListings(infoContainer, itemId, itemName)
}
function fetchMarketValueAndListings(container, itemId, itemName) {
const marketValuePromise = new Promise((resolve) => {
window.GM_xmlhttpRequest({
method: "GET",
url: `https://tornexchange.com/api/te_price?item_id=${itemId}`,
onload: (response) => {
let marketValue = ""
try {
const data = JSON.parse(response.responseText)
if (data && data.status === "success" && data.data && data.data.te_price) {
const rounded = Math.round(data.data.te_price)
marketValue = `$${rounded.toLocaleString()}`
}
} catch (e) {}
resolve(marketValue)
},
onerror: () => resolve(""),
})
})
const listingsPromise = new Promise((resolve) => {
window.GM_xmlhttpRequest({
method: "GET",
url: `https://weav3r.dev/api/marketplace/${itemId}`,
onload: (response) => {
try {
const data = JSON.parse(response.responseText)
if (!data || !data.listings) {
resolve(null)
return
}
const listings = data.listings.map((l) => ({
player_name: l.player_name,
player_id: l.player_id,
quantity: l.quantity,
price: l.price,
item_id: l.item_id,
}))
resolve(listings)
} catch (e) {
resolve(null)
}
},
onerror: () => resolve(null),
})
})
Promise.all([marketValuePromise, listingsPromise]).then(([marketValue, listings]) => {
if (marketValue) {
container.dataset.marketValue = marketValue
const marketValueDisplay = container.querySelector(".market-value-display")
if (marketValueDisplay) {
marketValueDisplay.textContent = `Market Value: ${marketValue}`
}
}
if (!listings) {
renderMessage(container, true)
return
}
allListings = listings
renderCards(container, marketValue)
})
}
function fetchBazaarListings(itemId, container, marketValue) {
// This function is no longer needed due to parallel API calls
}
function renderCards(container, marketValue) {
const cardContainer = container.querySelector(".bazaar-card-container")
if (!cardContainer) return
const controls = container.querySelector(".bazaar-controls")
const filters = {}
if (controls) {
const inputs = controls.querySelectorAll("input[type='number']")
inputs.forEach((input) => {
if (input.placeholder.includes("Min Price")) filters.minPrice = input.value
if (input.placeholder.includes("Max Price")) filters.maxPrice = input.value
if (input.placeholder.includes("Min Qty")) filters.minQty = input.value
if (input.placeholder.includes("Max Qty")) filters.maxQty = input.value
})
}
let processedListings = applyFilters(allListings, filters)
processedListings = sortListings(processedListings)
cardContainer.innerHTML = ""
if (!processedListings || processedListings.length === 0) {
cardContainer.innerHTML =
'<div style="color: #666; text-align: center; padding: 20px;">No bazaar listings available</div>'
return
}
const marketNum = marketValue
? Number.parseInt(marketValue.replace(/\D/g, ""))
: container.dataset.marketValue
? Number.parseInt(container.dataset.marketValue.replace(/\D/g, ""))
: null
processedListings.forEach((listing) => {
const card = document.createElement("div")
card.className = "bazaar-card"
const isVisited = window._visitedBazaars.has(listing.player_id)
let diffText = ""
if (marketNum && listing.price) {
const percent = (((listing.price - marketNum) / marketNum) * 100).toFixed(1)
const color = percent < 0 ? "limegreen" : "red"
const sign = percent > 0 ? "+" : ""
diffText = `<div class="bazaar-percentage" style="color: ${color};">${sign}${percent}%</div>`
}
const playerNameHtml = listing.player_name || "Unknown"
card.innerHTML = `
<div style="font-weight: bold; color: ${isVisited ? "#800080" : "#1E90FF"}; margin-bottom: 2px; line-height: 1.2; font-size: 16px;">${playerNameHtml}</div>
<div style="margin-bottom: 1px; font-size: 14px;">Qty: ${listing.quantity}</div>
<div style="font-size: 14px;">$${Math.round(listing.price).toLocaleString()}</div>
${diffText}
`
card.addEventListener("click", () => {
if (listing.player_id) {
window._visitedBazaars.add(listing.player_id)
const nameDiv = card.querySelector("div:first-child")
if (nameDiv) nameDiv.style.color = "#800080"
}
const bazaarUrl = `https://www.torn.com/bazaar.php?userId=${listing.player_id}&highlightItem=${listing.item_id}#/`
window.open(bazaarUrl, "_blank")
})
cardContainer.appendChild(card)
})
}
function processAllSellerWrappers(root = document.body) {
const currentUrl = window.location.href
console.log("[v0] Current URL:", currentUrl)
console.log("[v0] Is mobile view:", isMobileView)
const isItemPage =
currentUrl.includes("&XID=") ||
currentUrl.includes("/item.php") ||
currentUrl.includes("items.php?XID=") ||
(currentUrl.includes("items.php") && currentUrl.includes("XID")) ||
((currentUrl.includes("#/market/view=item") ||
currentUrl.includes("view=item") ||
(currentUrl.includes("ItemMarket") && currentUrl.includes("itemID="))) &&
!currentUrl.includes("view=category"))
console.log("[v0] Is item page:", isItemPage)
if (!isItemPage) {
console.log("[v0] Not an item page, skipping")
return
}
const existingContainer = document.querySelector(".bazaar-info-container")
if (existingContainer) {
console.log("[v0] Container already exists, skipping")
return
}
console.log("[v0] Processing seller wrappers...")
if (isMobileView) {
console.log("[v0] Processing mobile seller list")
processMobileSellerList()
}
const selectors = isMobileView
? 'ul.sellerList__e4C9, ul[class*="sellerList"], [class*="seller"], [class*="item"], [class*="wrapper"]'
: '[class*="sellerListWrapper"]'
const sellerWrappers = root.querySelectorAll(selectors)
console.log("[v0] Found seller wrappers:", sellerWrappers.length)
sellerWrappers.forEach((wrapper) => processSellerWrapper(wrapper))
}
function processSellerWrapper(wrapper) {
if (!wrapper || wrapper.dataset.bazaarProcessed) return
const currentUrl = window.location.href
if (!currentUrl.includes("&XID=") && !currentUrl.includes("/item.php")) {
return
}
const existingContainer = document.querySelector(".bazaar-info-container")
if (existingContainer) {
return
}
let itemTile, nameEl, btn, itemName, itemId
if (isMobileView) {
return
} else {
itemTile = wrapper.closest('[class*="itemTile"]') || wrapper.previousElementSibling
if (!itemTile) return
nameEl = itemTile.querySelector('div[class*="name"]') || itemTile.querySelector("div")
btn = itemTile.querySelector('button[aria-controls*="itemInfo"]')
if (!nameEl || !btn) return
itemName = nameEl.textContent.trim()
const idParts = btn.getAttribute("aria-controls").split("-")
itemId = idParts[idParts.length - 1]
}
if (!itemId) return
wrapper.dataset.bazaarProcessed = "true"
const infoContainer = createInfoContainer(itemName, itemId)
wrapper.parentNode.insertBefore(infoContainer, wrapper)
createFilters(infoContainer)
fetchMarketValueAndListings(infoContainer, itemId, itemName)
}
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()
}
} finally {
isProcessing = false
observer.processingTimeout = null
}
}, 100)
}
})
observer.observe(observeTarget, {
childList: true,
subtree: true,
})
if (checkMobileView()) {
processMobileSellerList()
}
})()
// --- Bazaar Page Green Highlight (Mobile Compatible) ---
;(() => {
const params = new URLSearchParams(window.location.search)
const itemIdToHighlight = params.get("highlightItem")
if (!itemIdToHighlight) return
const observer = new MutationObserver(() => {
const imgs = document.querySelectorAll("img")
imgs.forEach((img) => {
if (img.src.includes(`images/items/${itemIdToHighlight}/`)) {
img.closest("div")?.style.setProperty("outline", "3px solid green", "important")
img.scrollIntoView({ behavior: "smooth", block: "center" })
}
})
})
observer.observe(document.body, { childList: true, subtree: true })
})()