您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Bazaar listings on Item Page - Works Mobile/PDA
// ==UserScript== // @name PDA Bazaar Listing on Market(TE Market Value, No API Key) - Formatted Prices // @namespace https://weav3r.dev/ // @version 2.3.5 // @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 // @license MIT // ==/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 }) })()