Bazaar + TE Info PC Version

Adds Bazaar listings, TornExchange data, sorting/filtering to Item Market/Bazaar detail views. Optimized for PC/Desktop view.

当前为 2025-11-03 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Bazaar + TE Info PC Version
// @namespace    https://weav3r.dev/
// @version      2.2.1
// @description  Adds Bazaar listings, TornExchange data, sorting/filtering to Item Market/Bazaar detail views. Optimized for PC/Desktop view.
// @author       WTV [3281931]
// @match        https://www.torn.com/*
// @match        https://www.torn.com/itemmarket.php*
// @match        https://www.torn.com/bazaar.php*
// @grant        GM_xmlhttpRequest
// @connect      weav3r.dev
// @connect      tornexchange.com
// @connect      www.torn.com
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    
    console.log("[Bazaar + TE Info PC Version V 1.6 RUNNING].");

    // Global state
    window._visitedBazaars = window._visitedBazaars || new Set();
    window._cachedListings = window._cachedListings || {}; 
    window._marketValueCache = window._marketValueCache || {}; 
    window._activeSort = window._activeSort || { type: 'price', dir: 'asc' }; 
    
    let isMobileView = false;
    let currentDarkMode = document.body ? document.body.classList.contains("dark-mode") : false;
    let lastUrl = window.location.href; 
    let checkInterval = null;
    
    // ----------------------------------------------------------------------
    // --- UTILITY & STYLES ---
    // ----------------------------------------------------------------------
    
    function checkMobileView() {
        isMobileView = window.innerWidth <= 784; 
        return isMobileView;
    }
    
    // Initial check and resize listener set up
    checkMobileView();
    window.addEventListener("resize", checkMobileView);
    
    // Custom utility to wait for a specific DOM element to appear
    function waitForElement(selector, callback) {
        if (checkInterval) clearInterval(checkInterval);
        
        const MAX_ATTEMPTS = 50; // Check for up to 5 seconds
        let attempts = 0;
        
        const check = () => {
            const element = document.querySelector(selector);
            if (element) {
                if (checkInterval) clearInterval(checkInterval);
                callback(element);
            } else if (attempts++ >= MAX_ATTEMPTS) {
                if (checkInterval) clearInterval(checkInterval);
                console.log(`[Bazaar + TE Info PC Version] Timeout waiting for selector: ${selector}`);
            }
        };

        checkInterval = setInterval(check, 100);
        check(); // Initial check
    }

    function extractItemId() {
        let itemId = null;
        
        // Mobile View extraction (for internal flag/logic)
        if (isMobileView) {
            const btn = document.querySelector('[class*="itemsHeader"] button[aria-controls^="wai-itemInfo-"]');
            if (btn) {
                const controls = btn.getAttribute("aria-controls");
                const parts = controls.split("-");
                itemId = parts.length > 2 ? parts[parts.length - 2] : parts[parts.length - 1];
            }
            if (!document.querySelector('ul[class*="sellerList"]')) { return null; }
        } else { 
            // PC View extraction (used by the main flow)
            const itemDetails = document.querySelector('[class*="item-details"], [class*="item-info-desc"]');
            
            if (!itemDetails) { return null; }
            
            const itemImg = itemDetails.querySelector('img[src*="/images/items/"]');
            if (itemImg) {
                 const src = itemImg.getAttribute('src');
                 const imgMatch = src.match(/\/images\/items\/(\d+)\//i);
                 if (imgMatch && imgMatch[1]) { itemId = imgMatch[1]; }
            }
            
            if (!itemId) {
                const currentURL = new URL(window.location.href);
                const urlMatch = currentURL.search.match(/ID=(\d+)/i) || window.location.hash.match(/ID=(\d+)/i);
                if (urlMatch && urlMatch[1]) { itemId = urlMatch[1]; }
            }
        }
        
        return (itemId && itemId !== "unknown") ? itemId : null;
    }
    
    function updateStyles() {
        let styleEl = document.getElementById("bazaar-enhanced-styles");
        if (!styleEl) {
            styleEl = document.createElement("style");
            document.head.appendChild(styleEl);
        }
        styleEl.textContent = `
            .bazaar-info-container { border: 1px solid #888; margin: 5px 0 10px 0; padding: 5px; background: #222; color: #fff; border-radius: 4px; max-width: 100%; box-sizing: border-box; }
            .dark-mode .bazaar-info-container { background: #222; border-color: #444; }
            .bazaar-control-line { display: flex; flex-wrap: nowrap; gap: 4px; align-items: center; overflow-x: auto; padding-right: 5px; } 
            .bazaar-filter-toggle-btn { background: #555; color: white; border: none; padding: 4px 5px; cursor: pointer; font-weight: bold; height: 28px; width: 50px; flex-shrink: 0; font-size: 12px; border-radius: 4px; }
            .bazaar-filter-toggle-btn.active-filter { background: #007bff; }
            .bazaar-sort-btn { cursor:pointer; font-size: 14px; line-height: 1; margin: 0; padding: 0; }
            .bazaar-filter-input { padding: 4px; background: #333; border: 1px solid #444; color: white; height: 28px; box-sizing: border-box; font-size: 12px; border-radius: 4px; }
            .bazaar-filter-group-price input { width: 48px; } 
            .bazaar-filter-group-quantity input { width: 45px; } 
            .bazaar-apply-btn { background: #28a745; color: white; border: none; padding: 4px 8px; cursor: pointer; font-weight: bold; height: 28px; flex-shrink: 0; font-size: 12px; border-radius: 4px; }
            .bazaar-card-container { display:flex; overflow-x:auto; padding:5px; gap:5px; }
            .bazaar-card { border:1px solid #444; background:#222; color:#eee; padding: 2px 3px; margin:2px; width: 95px; flex-shrink: 0; cursor:pointer; display:flex; flex-direction:column; font-family: inherit; font-size:14px; transition:transform 0.2s; position: relative; gap: 0; border-radius: 4px; } 
            .bazaar-card a { overflow: hidden !important; white-space: nowrap !important; text-overflow: ellipsis; line-height: 1.1; font-size: 14px !important; padding: 0; margin: 0 0 3px 0; display: block; }
            .bazaar-card div:nth-child(2) { font-size: 14px !important; line-height: 1.0; padding: 0; margin: 3px 0 3px 0; font-weight: bold; }
            .bazaar-card div:nth-child(3) { font-size: 12px !important; line-height: 1.0; padding: 0; margin: 3px 0 0 0; display: flex; justify-content: space-between; align-items: baseline; }
            .bazaar-title { font-size: 16px !important; font-weight: bold !important; flex-grow: 1; overflow: visible; white-space: normal;}
            .bazaar-info-header { font-weight:bold;margin-bottom:5px; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; font-size: 14px; overflow: visible; }
            .bazaar-te-id-line { display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; }
            .bazaar-te-id-line .item-id-display { color:#aaa; font-size:13px; font-weight: bold; white-space: nowrap; }
            .bazaar-filter-inputs-group { display: flex; gap: 2px; align-items: center; flex-wrap: nowrap; flex-grow: 1; justify-content: flex-end; flex-shrink: 0; }
            .bazaar-filter-group-price, .bazaar-filter-group-quantity { display: flex; align-items:center; gap:2px; flex-shrink: 0; }
            .best-buyer-line { font-weight:bold; margin-bottom: 5px; color:#FFA500; display: flex; flex-wrap: nowrap; justify-content: flex-start; align-items: center; gap: 5px; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
            .best-buyer-line > span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
            .best-buyer-line > span:first-child, .best-buyer-line .price-display-span { flex-shrink: 0; }
            .best-buyer-line a { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; }
        `;
    }
    // Update styles on initial run (if head exists)
    if (document.head) 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();
                }
            }
        });
    });
    // Observer starts checking for body class change as soon as body exists
    new MutationObserver((mutations, obs) => {
          if (document.body) {
              darkModeObserver.observe(document.body, { attributes: true });
              obs.disconnect();
              currentDarkMode = document.body.classList.contains("dark-mode");
              updateStyles();
          }
    }).observe(document.documentElement, { childList: true });

    function renderNoListingsMessage(container, isError){
        const cardContainer = container.querySelector('.bazaar-card-container');
        if(!cardContainer) return;
        cardContainer.innerHTML = '';
        const msg = document.createElement('div');
        msg.style.cssText='color:#fff;text-align:center;padding:20px;width:100%;';
        msg.innerHTML = isError ? "API Error<br><span style='font-size:12px;color:#ccc;'>Could not fetch bazaar data.</span>"
                                 : "No bazaar listings available for this item.";
        cardContainer.appendChild(msg);
    }
    
    function fetchUpvoteCountAndId(userName, callback) {
        const url = `https://tornexchange.com/prices/${userName}/`;
        GM_xmlhttpRequest({
            method: "GET", url: url,
            onload: function(response) {
                let upvoteCount = null;
                let traderId = null;
                try {
                    const doc = new DOMParser().parseFromString(response.responseText, "text/html");
                    const upvoteElement = doc.querySelector('#vote-score');
                    if (upvoteElement) { upvoteCount = upvoteElement.textContent.trim(); }
                    const profileLink = doc.querySelector('a[href*="profiles.php?XID="]');
                    if (profileLink) {
                        const href = profileLink.getAttribute('href');
                        const match = href.match(/XID=(\d+)/);
                        if (match && match[1]) { traderId = match[1]; }
                    }
                } catch (e) { /* silent fail, as per V 1.0 */ }
                callback(upvoteCount, traderId);
            },
            onerror: function() { callback(null, null); }
        });
    }

    function createInfoContainer(itemName, itemId, marketValue, bestBuyer, upvoteCount, traderId) {
        const container = document.createElement('div');
        container.className = 'bazaar-info-container';
        container.dataset.itemid = itemId;
        if (marketValue) container.dataset.marketValue = marketValue; 
        if (traderId) container.dataset.bestBuyerId = traderId;
        
        const marketText = marketValue ? ` <span style="color:#FFD700; flex-shrink: 0; font-size: 14px;">(MV: ${marketValue})</span>` : '';
        const encodedItemName = encodeURIComponent(itemName);
        const teListingsLink = `https://tornexchange.com/listings?model_name_contains=${encodedItemName}&order_by=&status=`;
        const teListingHTML = `<a href="${teListingsLink}" target="_blank" style="color:#00BFFF; font-size: 13px; text-decoration: none; white-space:nowrap; font-weight: bold;">TE Listings</a>`;
        const itemIdHTML = `<span class="item-id-display">Item #: ${itemId}</span>`;
        
        const teIdLineHTML = `<div class="bazaar-te-id-line" style="margin-bottom: 5px;">${teListingHTML}${itemIdHTML}</div>`;
        
        let bestBuyerInfoHTML = '';
        if (bestBuyer && bestBuyer.price) {
            const formattedPrice = `$${Math.round(bestBuyer.price).toLocaleString()}`;
            const priceDisplayHTML = `<span class="price-display-span" style="color:lime; font-weight:bold; white-space:nowrap; font-size: 16px;">${formattedPrice}</span>`;
            if (bestBuyer.trader && traderId) {
                const traderLinkHTML = `<a href="https://www.torn.com/profiles.php?XID=${traderId}" target="_blank" style="color:#1E90FF; text-decoration:none; font-weight:bold; cursor:pointer;" onmouseover="this.style.textDecoration='underline';" onmouseout="this.style.textDecoration='none';">${bestBuyer.trader}</a>`;
                const upvoteText = upvoteCount ? ` (⭐ ${upvoteCount})` : ''; 
                bestBuyerInfoHTML = `<span style="flex-shrink: 0;">Best Trader: ${priceDisplayHTML}</span><span style="flex-grow: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;">by ${traderLinkHTML}${upvoteText}</span>`;
            } else {
                 bestBuyerInfoHTML = `<span style="flex-shrink: 0;">Best Trader: ${formattedPrice}</span>`;
                }
        } else {
             bestBuyerInfoHTML = `<span style="flex-shrink: 0;">No Best Trader Data</span>`;
        }
        const bestBuyerHTML = `<div class="best-buyer-line">${bestBuyerInfoHTML}</div>`;
        
        const filterControlsHTML = `
            <div class="bazaar-control-row" style="display:flex; flex-direction: column; gap:8px; margin: 5px 0 8px 0;">
                <div class="bazaar-control-line">
                    
                    <div class="bazaar-price-controls" style="display: flex; gap: 4px; align-items: center; flex-shrink: 0; white-space: nowrap;">
                        <button class="bazaar-filter-toggle-btn active-filter" data-filter-type="price" style="background:#007bff;">Price</button>
                        <div style="display: flex; flex-direction: column; height: 28px; justify-content: center; align-items: center; padding-right: 4px;">
                            <span class="bazaar-sort-btn" data-sort-by="price" data-sort-dir="asc" style="color:#00BFFF;">🔼</span>
                            <span class="bazaar-sort-btn" data-sort-by="price" data-sort-dir="desc" style="color:#555;">🔽</span>
                        </div>
                    </div>
                    
                    <div class="bazaar-quantity-controls" style="display: flex; gap: 4px; align-items: center; flex-shrink: 0; white-space: nowrap;">
                        <button class="bazaar-filter-toggle-btn" data-filter-type="quantity">Qty</button>
                        <div style="display: flex; flex-direction: column; height: 28px; justify-content: center; align-items: center; padding-right: 4px;">
                            <span class="bazaar-sort-btn" data-sort-by="quantity" data-sort-dir="asc" style="color:#555;">🔼</span>
                            <span class="bazaar-sort-btn" data-sort-by="quantity" data-sort-dir="desc" style="color:#555;">🔽</span>
                        </div>
                    </div>
                    
                    <div class="bazaar-filter-inputs-group">
                        <div class="bazaar-filter-group-price">
                            <input type="number" placeholder="Min" class="bazaar-filter-input" data-filter-type="minPrice">
                            <input type="number" placeholder="Max" class="bazaar-filter-input" data-filter-type="maxPrice">
                        </div>
                        <div class="bazaar-filter-group-quantity" style="display: none;">
                            <input type="number" placeholder="Min" class="bazaar-filter-input" data-filter-type="minQty">
                            <input type="number" placeholder="Max" class="bazaar-filter-input" data-filter-type="maxQty">
                        </div>
                        <button class="bazaar-apply-btn" style="display: block;">Apply</button>
                    </div>
                </div>
            </div>
        `;

        container.innerHTML = `
            <div class="bazaar-info-header">
                <span class="bazaar-title">${itemName}${marketText}</span>
            </div>
            ${bestBuyerHTML}
            ${teIdLineHTML}
            ${filterControlsHTML}
            <div class="bazaar-card-container"></div>
        `;

        const cardContainer = container.querySelector('.bazaar-card-container');
        if (cardContainer) {
            cardContainer.addEventListener("wheel", e => {
                if (e.deltaY !== 0) { e.preventDefault(); cardContainer.scrollLeft += e.deltaY; }
            });
        }

        addFilterListeners(container, itemId);
        return container;
    }

    function sortAndFilterListings(itemId, container) {
        let listings = window._cachedListings[itemId];
        if (!listings) {
            renderNoListingsMessage(container, !window._cachedListings[itemId]);
            return; 
        }
        
        const sortType = window._activeSort.type;
        const sortDir = window._activeSort.dir;
        
        const minPrice = parseFloat(container.querySelector('[data-filter-type="minPrice"]').value) || null;
        const maxPrice = parseFloat(container.querySelector('[data-filter-type="maxPrice"]').value) || null;
        const minQty = parseInt(container.querySelector('[data-filter-type="minQty"]').value) || null;
        const maxQty = parseInt(container.querySelector('[data-filter-type="maxQty"]').value) || null;
        
        let filteredListings = listings.slice().filter(listing => {
            const price = parseFloat(listing.price.toString().replace(/,/g, ''));
            const qty = parseInt(listing.quantity);
            if (minPrice !== null && price < minPrice) return false;
            if (maxPrice !== null && price > maxPrice) return false;
            if (minQty !== null && qty < minQty) return false;
            if (maxQty !== null && qty > maxQty) return false;
            return true;
        });

        filteredListings.sort((a, b) => {
            let primaryValA, primaryValB;
            if (sortType === 'price') {
                primaryValA = parseFloat(a.price.toString().replace(/,/g, ''));
                primaryValB = parseFloat(b.price.toString().replace(/,/g, ''));
            } else { // 'quantity'
                primaryValA = parseInt(a.quantity);
                primaryValB = parseInt(b.quantity);
            }
            let comparison = 0;
            if (sortDir === 'asc') { comparison = primaryValA - primaryValB; } else if (sortDir === 'desc') { comparison = primaryValB - primaryValA; }
            
            if (comparison === 0) {
                 const priceA = parseFloat(a.price.toString().replace(/,/g, ''));
                 const priceB = parseFloat(b.price.toString().replace(/,/g, ''));
                 return priceA - priceB;
            }
            return comparison;
        });
        
        const marketNum = window._marketValueCache?.[itemId] || null;
        renderCards(container, filteredListings, marketNum);
    }
    
    function addFilterListeners(container, itemId) {
        const priceFilterGroup = container.querySelector('.bazaar-filter-group-price');
        const quantityFilterGroup = container.querySelector('.bazaar-filter-group-quantity');
        const sortBtns = container.querySelectorAll('.bazaar-sort-btn'); 
        const filterToggleBtns = container.querySelectorAll('.bazaar-filter-toggle-btn');
        const applyBtn = container.querySelector('.bazaar-apply-btn');
        const defaultColor = '#555';
        const activeColor = '#00BFFF'; 
        const reverseColor = '#dc3545'; 

        const updateSortVisuals = () => {
             filterToggleBtns.forEach(btn => {
                 const filterType = btn.dataset.filterType;
                 if (filterType === window._activeSort.type) { 
                      btn.style.background = '#007bff';
                      btn.classList.add('active-filter');
                 } else {
                      btn.style.background = defaultColor;
                      btn.classList.remove('active-filter');
                 }
             });
             sortBtns.forEach(btn => {
                 const type = btn.dataset.sortBy;
                 const dir = btn.dataset.sortDir;
                 let color = defaultColor;
                 if (type === window._activeSort.type && dir === window._activeSort.dir) {
                     color = dir === 'asc' ? activeColor : reverseColor;
                 } else if (type === window._activeSort.type) {
                     color = '#777'; 
                 }
                 btn.style.color = color;
             });
        };

        sortBtns.forEach(btn => {
            btn.addEventListener('click', (e) => {
                window._activeSort.type = btn.dataset.sortBy;
                window._activeSort.dir = btn.dataset.sortDir;
                updateSortVisuals();
                sortAndFilterListings(itemId, container);
            });
        });

        filterToggleBtns.forEach(btn => {
            btn.addEventListener('click', (e) => {
                const filterType = btn.dataset.filterType;
                priceFilterGroup.style.display = (filterType === 'price') ? 'flex' : 'none';
                quantityFilterGroup.style.display = (filterType === 'quantity') ? 'flex' : 'none';
                applyBtn.style.display = 'block';
                updateSortVisuals();
                sortAndFilterListings(itemId, container);
            });
        });

        applyBtn.addEventListener('click', () => { sortAndFilterListings(itemId, container); });

        container.querySelectorAll('.bazaar-filter-input').forEach(input => {
            input.addEventListener('keydown', (e) => {
                if (e.key === 'Enter') { e.preventDefault(); input.blur(); sortAndFilterListings(itemId, container); }
            });
            input.addEventListener('blur', () => sortAndFilterListings(itemId, container));
        });
        updateSortVisuals();
    }

    function renderCards(container, listings, marketNum){
        const cardContainer=container.querySelector('.bazaar-card-container');
        if(!cardContainer || !listings) return;
        cardContainer.innerHTML='';
        
        if(listings.length===0){
            const msg = document.createElement('div');
            msg.style.cssText='color:#fff;text-align:center;padding:20px;width:100%;';
            msg.innerHTML = "No bazaar listings match the current filters.";
            cardContainer.appendChild(msg);
            return;
        }

        const bestBuyerId = container.dataset.bestBuyerId;
        listings.forEach(listing=>{
            const card=document.createElement('div');
            card.className = "bazaar-card";
            const isVisited=window._visitedBazaars.has(listing.player_id);
            const isBestBuyer = bestBuyerId && listing.player_id == bestBuyerId;
            const bazaarLink = `https://www.torn.com/bazaar.php?userId=${listing.player_id}&highlightItem=${listing.item_id}#/`;
            
            if (isBestBuyer) { card.style.cssText += `border: 2px solid #28a745 !important; background: #333 !important;`; }

            const priceNum = parseFloat(listing.price.toString().replace(/,/g, ''));
            const formattedPrice = `$${Math.round(priceNum).toLocaleString()}`;
            
            let diffTextHTML = '';
            if(marketNum){
                const percent = ((priceNum - marketNum)/marketValue.te_price*100).toFixed(1);
                let color = 'gold'; 
                if (percent < -0.5) { color = 'limegreen'; } else if (percent > 0.5) { color = 'red'; } 
                const sign = percent > 0 ? '+' : '';
                diffTextHTML = `<span style="font-weight:bold; color:${color}; font-size: 12px; margin-left: auto; white-space: nowrap; flex-shrink: 0;">${sign}${percent}%</span>`;
            }

            card.innerHTML=`
                <a href="${bazaarLink}" target="_blank"
                    style="font-weight:bold; color:${isVisited?'#800080':'#1E90FF'}; text-decoration:none; cursor:pointer; 
                           overflow: hidden; white-space: nowrap; text-overflow: ellipsis; 
                           line-height: 1.1; padding: 0; margin: 0 0 3px 0; display: block;">
                    ${listing.player_name || 'Unknown'}
                </a>
                <div style="font-weight: bold; color: #eee; white-space: nowrap; line-height: 1.0; padding: 0; margin: 3px 0 3px 0;">${formattedPrice}</div>
                <div style="display: flex; justify-content: space-between; align-items: baseline; line-height: 1.0; padding: 0; margin: 3px 0 0 0;">
                    <span style="white-space: nowrap;">Qty: ${listing.quantity}</span>
                    ${diffTextHTML}
                </div>
            `;
            
            card.addEventListener('click', (e)=>{
                if(e.target.tagName === 'A') return;
                if(listing.player_id){
                    window._visitedBazaars.add(listing.player_id);
                    const nameLink = card.querySelector('a:first-child');
                    if(nameLink) nameLink.style.color='#800080';
                }
                window.open(bazaarLink, '_blank');
            });
            cardContainer.appendChild(card);
        });
    }

    // ----------------------------------------------------------------------
    // --- CORE FETCH & DOM MANIPULATION ---
    // ----------------------------------------------------------------------
    
    function fetchTornExchangeData(itemId, callback) {
        let marketValue = null; 
        let bestBuyer = null;
        let requestsCompleted = 0;
        
        const checkCompletion = () => {
             requestsCompleted++;
             if (requestsCompleted === 2) {
                 if (bestBuyer && bestBuyer.trader) {
                     fetchUpvoteCountAndId(bestBuyer.trader, (count, id) => {
                         callback(marketValue, bestBuyer, count, id);
                     });
                 } else {
                     callback(marketValue, bestBuyer, null, null);
                 }
             }
        };
        
        GM_xmlhttpRequest({
            method: "GET", url: `https://tornexchange.com/api/te_price?item_id=${itemId}`,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data && data.status === 'success' && data.data && data.data.te_price) { 
                        window._marketValueCache[itemId] = data.data.te_price; 
                        marketValue = `$${Math.round(data.data.te_price).toLocaleString()}`; 
                    }
                } catch (e) { /* silent fail, as per V 1.0 */ }
                checkCompletion();
            },
            onerror: function() { checkCompletion(); }
        });
        
        GM_xmlhttpRequest({
            method: "GET", url: `https://tornexchange.com/api/best_listing?item_id=${itemId}`,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data && data.status === 'success' && data.data) {
                        if (data.data.price) { bestBuyer = { price: data.data.price, trader: data.data.trader || null }; }
                    }
                } catch (e) { /* silent fail, as per V 1.0 */ }
                checkCompletion();
            },
            onerror: function() { checkCompletion(); }
        });
    }

    function fetchBazaarListings(itemId, infoContainer) {
        GM_xmlhttpRequest({
            method:"GET",
            url:`https://weav3r.dev/api/marketplace/${itemId}`,
            onload:function(response){
                try{
                    const data = JSON.parse(response.responseText); 
                    const listingsReceived = data.listings ? data.listings.length : 0;
                    
                    if(!data || !data.listings || listingsReceived === 0){
                        renderNoListingsMessage(infoContainer, false);
                        return;
                    }
                    
                    const allListings = 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
                    }));

                    window._cachedListings[itemId] = allListings;
                    sortAndFilterListings(itemId, infoContainer); 

                } catch(e){
                    console.error(`[Bazaar + TE Info PC Version Error] Failed to process Weaver API response for item ${itemId}:`, e);
                    renderNoListingsMessage(infoContainer, true);
                }
            },
            onerror:function(error){
                console.error(`[Bazaar + TE Info PC Version Error] GM_xmlhttpRequest failed for item ${itemId}:`, error);
                renderNoListingsMessage(infoContainer, true);
            }
        });
    }

    function updateInfoContainer(wrapper, itemId, itemName) {
        const infoContainer = createInfoContainer(itemName, itemId, null, null, null, null);
        wrapper.insertBefore(infoContainer, wrapper.firstChild);

        fetchTornExchangeData(itemId, (marketValue, bestBuyer, upvoteCount, traderId) => {
            
            let bestBuyerInfoHTML = '';
            if (bestBuyer && bestBuyer.price) {
                const formattedPrice = `$${Math.round(bestBuyer.price).toLocaleString()}`;
                const priceDisplayHTML = `<span class="price-display-span" style="color:lime; font-weight:bold; white-space:nowrap; font-size: 16px;">${formattedPrice}</span>`;
                if (bestBuyer.trader && traderId) {
                    const traderLinkHTML = `<a href="https://www.torn.com/profiles.php?XID=${traderId}" target="_blank" style="color:#1E90FF; text-decoration:none; font-weight:bold; cursor:pointer;" onmouseover="this.style.textDecoration='underline';" onmouseout="this.style.textDecoration='none';">${bestBuyer.trader}</a>`;
                    const upvoteText = upvoteCount ? ` (⭐ ${upvoteCount})` : ''; 
                    bestBuyerInfoHTML = `<span style="flex-shrink: 0;">Best Trader: ${priceDisplayHTML}</span><span style="flex-grow: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;">by ${traderLinkHTML}${upvoteText}</span>`;
                } else {
                     bestBuyerInfoHTML = `<span style="flex-shrink: 0;">Best Trader: ${formattedPrice}</span>`;
                }
            } else {
                 bestBuyerInfoHTML = `<span style="flex-shrink: 0;">No Best Trader Data</span>`;
            }

            const newBestBuyerHTML = `<div class="best-buyer-line">${bestBuyerInfoHTML}</div>`;
            const oldBestBuyer = infoContainer.querySelector('.best-buyer-line');
            if (oldBestBuyer && oldBestBuyer.parentNode) {
                 const tempDiv = document.createElement('div');
                 tempDiv.innerHTML = newBestBuyerHTML;
                 oldBestBuyer.parentNode.replaceChild(tempDiv.firstChild, oldBestBuyer);
            }
            
            const marketText = marketValue ? ` (MV: ${marketValue})` : '';
            const itemNameEl = infoContainer.querySelector('.bazaar-title');
            if (itemNameEl) {
                itemNameEl.innerHTML = `${itemName}<span style="color:#FFD700; flex-shrink: 0; font-size: 14px;">${marketText}</span>`;
            }
            
            if (window._cachedListings[itemId]) {
                sortAndFilterListings(itemId, infoContainer);
            }
        });

        fetchBazaarListings(itemId, infoContainer);
    }

    // ----------------------------------------------------------------------
    // --- MAIN EXECUTION & MONITORS ---
    // ----------------------------------------------------------------------
    
    // Main selector for the market page's content wrapper on PC view
    const PC_CONTENT_WRAPPER_SELECTOR = '.content-wrapper > .content:has([class*="item-info-desc"], [class*="item-details"])';
    const MOBILE_CONTENT_WRAPPER_SELECTOR = 'ul[class*="sellerList"]'; // Use a mobile-specific selector as a backup

    function processBazaarOrMarketItem(wrapperElement) {
        
        if (!window.location.href.match(/(itemmarket|bazaar|page)\.php/i)) return;
        
        let wrapper = null;
        let itemName = '';
        
        // --- 1. Determine View and Content Wrapper ---
        if (checkMobileView() && document.querySelector(MOBILE_CONTENT_WRAPPER_SELECTOR)) {
             wrapper = document.querySelector(MOBILE_CONTENT_WRAPPER_SELECTOR).parentNode;
             const headerEl = document.querySelector('.itemsHeader__ZTO9r .title__ruNCT, [class*="itemsHeader"] [class*="title"]');
             itemName = headerEl ? headerEl.textContent.trim() : "Unknown";
        } else if (!checkMobileView() && wrapperElement) {
             wrapper = wrapperElement;
             const itemDetails = wrapper.querySelector('[class*="item-details"], [class*="item-info-desc"]');
             if (itemDetails) {
                 const nameEl = itemDetails.querySelector('h3.item-name, [class*="item-name"], .title');
                 if (nameEl) { itemName = nameEl.textContent.trim(); }
             }
        }
        
        if (!wrapper || !itemName) {
            // console.log("[Bazaar + TE Info PC Version] Wrapper or Item Name not found.");
            return;
        }
        
        // --- 2. Extract Item ID ---
        const itemId = extractItemId();
        if (!itemId) return;

        // --- 3. Check for existing container ---
        const existingContainer = document.querySelector(`.bazaar-info-container`);
        const existingContainerForCurrentItem = document.querySelector(`.bazaar-info-container[data-itemid="${itemId}"]`);

        if (existingContainerForCurrentItem) {
             return; // Already processed this item
        }

        if (existingContainer && existingContainer.dataset.itemid !== itemId) {
             console.log(`[Bazaar + TE Info PC Version] Removing old container for item ${existingContainer.dataset.itemid}.`);
             existingContainer.remove();
        }

        // --- 4. Inject and Fetch ---
        if (!document.querySelector('.bazaar-info-container')) {
             updateInfoContainer(wrapper, itemId, itemName);
        }
    }
    
    // Primary execution loop using the robust element waiter
    function runScript() {
        if (window.location.href.match(/(itemmarket|bazaar|page)\.php/i)) {
             // Wait for the main content wrapper that confirms we are on a detail page.
             waitForElement(PC_CONTENT_WRAPPER_SELECTOR + ', ' + MOBILE_CONTENT_WRAPPER_SELECTOR, (wrapperElement) => {
                 processBazaarOrMarketItem(wrapperElement);
             });
        }
    }

    // URL Monitor (This handles navigation within Torn)
    setInterval(() => {
        if (window.location.href !== lastUrl) {
            // Cleanup old container when URL changes
            document.querySelectorAll('.bazaar-info-container').forEach(el => el.remove());
            lastUrl = window.location.href;
            window._cachedListings = {}; 
            window._marketValueCache = {}; 
            window._activeSort = { type: 'price', dir: 'asc' }; 
            // Restart detection process
            runScript(); 
        }
    }, 500); 

    // Mutation Observer (This handles when Torn loads new content into the existing URL)
    const observer = new MutationObserver((mutations) => {
        // Find the main content wrapper on mutation and run the process
        const wrapperElement = document.querySelector(PC_CONTENT_WRAPPER_SELECTOR) || document.querySelector(MOBILE_CONTENT_WRAPPER_SELECTOR)?.parentNode;
        if (wrapperElement && !document.querySelector('.bazaar-info-container')) {
            processBazaarOrMarketItem(wrapperElement);
        }
    });

    // Start observing a parent element that is always present early
    new MutationObserver((mutations, obs) => {
        if (document.body) {
            observer.observe(document.body, {childList:true,subtree:true});
            obs.disconnect();
            runScript(); // Initial run
        }
    }).observe(document.documentElement, { childList: true });
    
})();

// --- Bazaar Page Green Highlight (Kept robust with observer) ---
;(() => {
    const params = new URLSearchParams(window.location.search);
    const itemIdToHighlight = params.get("highlightItem");
    if (!itemIdToHighlight) return;
    
    const highlightObserver = 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" });
            }
        });
    });
    // Use an observer to start watching as soon as the document is ready
    new MutationObserver((mutations, obs) => {
        if (document.body) {
            highlightObserver.observe(document.body, { childList: true, subtree: true });
            obs.disconnect();
        }
    }).observe(document.documentElement, { childList: true });
})();