TEST

Shows Bazaar listing on Item Market with TE Data, offering sort/filter controls and a permanent player-link visited state. Optimized with Promises and better structure, with clean CSS separation.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name          TEST
// @namespace     https://weav3r.dev/
// @version       2.2.4
// @description   Shows Bazaar listing on Item Market with TE Data, offering sort/filter controls and a permanent player-link visited state. Optimized with Promises and better structure, with clean CSS separation.
// @author        WTV [3281931]
// @match         https://www.torn.com/*
// @grant         GM_xmlhttpRequest
// @grant         GM_addStyle
// @run-at        document-start
// @license       MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- CSS INJECTION: Refactored styles from inline to a single block for maintainability ---
    GM_addStyle(`
        .bazaar-info-container {
            border: 1px solid #888;
            margin: 10px 0;
            padding: 5px;
            background: #222;
            color: #fff;
        }

        .bazaar-info-header {
            font-weight: bold;
            margin-bottom: 5px;
            display: flex;
            flex-wrap: nowrap;
            justify-content: space-between;
            align-items: center;
            font-size: 14px;
        }

        .bazaar-title {
            flex-grow: 1;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        .bazaar-market-value {
            color: #FFD700;
            flex-shrink: 0;
            font-size: 14px;
        }

        .bazaar-count-info {
            font-size: 13px;
            color: #aaa;
            font-weight: normal;
            flex-shrink: 0;
            margin-left: 10px;
        }

        .best-buyer-line {
            font-weight: bold;
            margin-bottom: 5px;
            color: #FFA500;
            display: flex;
            flex-wrap: wrap;
            justify-content: flex-start;
            align-items: center;
            gap: 5px;
            font-size: 14px;
        }

        .best-buyer-line .price-display {
            color: lime;
            font-weight: bold;
            white-space: nowrap;
            font-size: 16px;
        }

        .best-buyer-line .trader-link {
            color: #1E90FF;
            text-decoration: none;
            font-weight: bold;
            cursor: pointer;
        }

        .best-buyer-line .te-listings-link {
            color: #00BFFF;
            font-size: 14px;
            text-decoration: none;
            white-space: nowrap;
            margin-left: 5px;
            font-weight: bold;
        }

        .bazaar-item-id {
            color: #aaa;
            font-size: 13px;
            font-weight: bold;
            white-space: nowrap;
            margin-left: auto;
        }

        .bazaar-control-row {
            display: flex;
            flex-direction: column;
            gap: 8px;
            margin: 5px 0 8px 0;
        }

        .bazaar-control-line {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            align-items: center;
        }

        .bazaar-price-controls, .bazaar-quantity-controls {
            display: flex;
            gap: 4px;
            align-items: center;
            flex-shrink: 0;
            white-space: nowrap;
        }

        .bazaar-filter-toggle-btn {
            background: #555;
            color: white;
            border: none;
            padding: 4px 8px;
            cursor: pointer;
            font-weight: bold;
            height: 28px;
            width: 65px;
            font-size: 12px;
            transition: background 0.2s;
        }

        .bazaar-sort-visuals {
            display: flex;
            flex-direction: column;
            height: 28px;
            justify-content: center;
            align-items: center;
            padding-right: 4px;
        }

        .bazaar-sort-btn {
            color: #555;
            font-weight: bold;
            cursor: pointer;
            font-size: 14px;
            line-height: 1;
            margin: 0;
            padding: 0;
            transition: color 0.2s;
        }

        .bazaar-filter-inputs-group {
            display: flex;
            gap: 8px;
            align-items: center;
            flex-wrap: wrap;
        }

        .bazaar-filter-group-price, .bazaar-filter-group-quantity {
            display: none;
            align-items: center;
            gap: 4px;
            flex-shrink: 0;
        }

        .bazaar-filter-input {
            width: 70px;
            padding: 4px;
            background: #333;
            border: 1px solid #444;
            color: white;
            height: 28px;
            box-sizing: border-box;
            font-size: 12px;
        }
        .bazaar-filter-group-quantity .bazaar-filter-input {
            width: 60px;
        }

        .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;
            display: none;
        }

        .bazaar-reset-all-btn {
            background: #444;
            color: white;
            border: none;
            padding: 4px 8px;
            cursor: pointer;
            font-weight: bold;
            height: 28px;
            flex-shrink: 0;
            font-size: 14px;
            margin-left: 5px;
        }

        .bazaar-card-container {
            display: flex;
            overflow-x: auto;
            padding: 5px;
            gap: 5px;
        }
        .bazaar-card-container::-webkit-scrollbar {
            height: 8px;
        }
        .bazaar-card-container::-webkit-scrollbar-thumb {
            background: #555;
            border-radius: 4px;
        }

        .bazaar-card {
            border: 1px solid #444;
            background: #222;
            color: #eee;
            padding: 10px;
            margin: 2px;
            width: 125px;
            flex-shrink: 0;
            cursor: pointer;
            display: flex;
            flex-direction: column;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            font-size: 15px;
            transition: transform 0.2s, border 0.2s, background 0.2s;
            position: relative;
            gap: 3px;
        }
        .bazaar-card:not(.is-best-buyer):hover {
            border-color: #555 !important;
            background: #2a2a2a !important;
        }

        .bazaar-card.is-best-buyer {
            border: 2px solid #28a745 !important;
            background: #333 !important;
        }
        .bazaar-card.is-best-buyer:hover {
            background: #3a3a3a !important;
        }

        .bazaar-card a {
            font-weight: bold;
            text-decoration: none;
            cursor: pointer;
            font-size: 15px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        .bazaar-card a:link { color: #1E90FF; }
        .bazaar-card a:visited { color: #800080; }
        .bazaar-card a:hover { text-decoration: underline; }

        .bazaar-card .price-info {
            font-size: 14px;
            white-space: nowrap;
        }

        .bazaar-card .qty-diff-info {
            font-size: 14px;
            display: flex;
            justify-content: space-between;
            align-items: baseline;
            line-height: 1;
            margin-bottom: 0;
        }

        .bazaar-card .diff-text-positive { color: red; font-weight: bold; }
        .bazaar-card .diff-text-negative { color: limegreen; font-weight: bold; }
        .bazaar-card .diff-text-neutral { color: gold; font-weight: bold; }
    `);
    // --- END CSS INJECTION ---

    // Global State Variables
    window._visitedBazaars = new Set();
    window._cachedListings = {};
    window._activeSort = {
        type: 'price',
        dir: 'asc'
    };

    // --- UTILITY FUNCTIONS ---

    /**
     * Converts GM_xmlhttpRequest into a standard Promise.
     * @param {string} url - The API endpoint URL.
     * @returns {Promise<Object|null>} - JSON data or null on failure.
     */
    function fetchApi(url) {
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data && data.status === 'success') {
                            resolve(data);
                        } else {
                            resolve(null);
                        }
                    } catch (e) {
                        resolve(null);
                    }
                },
                onerror: function() {
                    resolve(null);
                }
            });
        });
    }

    // --- API DATA FETCHING (Optimized with async/await) ---

    async function fetchProfileData(userId) {
        const url = `https://tornexchange.com/api/profile?user_id=${userId}`;
        const data = await fetchApi(url);

        if (data && data.data) {
            return {
                votes: data.data.votes,
                torn_id: data.data.torn_id // The Torn XID required for the link
            };
        }
        return { votes: null, torn_id: null };
    }

    async function fetchTornExchangeData(itemId) {
        let marketValue = '';
        let bestBuyer = null;
        let upvoteCount = null;
        let traderId = null;

        // Execute te_price and best_listing concurrently
        const [tePriceData, bestListingData] = await Promise.all([
            fetchApi(`https://tornexchange.com/api/te_price?item_id=${itemId}`),
            fetchApi(`https://tornexchange.com/api/best_listing?item_id=${itemId}`)
        ]);

        // 1. Process TE Price
        if (tePriceData && tePriceData.data && tePriceData.data.te_price) {
            const rounded = Math.round(tePriceData.data.te_price);
            marketValue = `$${rounded.toLocaleString()}`;
        }

        // 2. Process Best Listing
        if (bestListingData && bestListingData.data && bestListingData.data.price) {
            const buyerTeId = bestListingData.data.trader_id;

            bestBuyer = {
                price: bestListingData.data.price,
                trader: bestListingData.data.trader || null,
                player_id: buyerTeId || null
            };

            // 3. Fetch Profile Data (Conditional and Awaited)
            if (buyerTeId) {
                const profile = await fetchProfileData(buyerTeId);
                upvoteCount = profile.votes;
                traderId = profile.torn_id;
            }
        }

        return { marketValue, bestBuyer, upvoteCount, traderId };
    }


    // --- RENDERING FUNCTIONS ---

    function renderMessage(container, isError){
        const cardContainer = container.querySelector('.bazaar-card-container');
        if(!cardContainer) return;
        cardContainer.innerHTML = '';
        const msg = document.createElement('div');
        msg.className = 'bazaar-message';
        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. (Weaver API)</span>"
                                 : "No bazaar listings available for this item.";
        cardContainer.appendChild(msg);

        const countSpan = container.querySelector('.bazaar-count-info');
        if(countSpan) countSpan.textContent = '';
    }

    function createBestBuyerHTML(bestBuyer, upvoteCount, traderId, encodedItemName) {
        const listingsLink = `https://tornexchange.com/listings?model_name_contains=${encodedItemName}&order_by=&status=`;
        const teListingsLinkHTML = `<a href="${listingsLink}" target="_blank" class="te-listings-link">(TE Listings)</a>`;
        let bestBuyerInfoHTML = '';

        if (bestBuyer && bestBuyer.price && bestBuyer.trader) {
            const formattedPrice = `$${Math.round(bestBuyer.price).toLocaleString()}`;
            const traderName = bestBuyer.trader;
            let upvoteText = upvoteCount ? ` (⭐ ${upvoteCount} Upvotes)` : '';

            if (traderId) {
                const profileLink = `https://www.torn.com/profiles.php?XID=${traderId}`;
                const traderLinkHTML = `
                    <a href="${profileLink}" target="_blank" class="trader-link"
                        onmouseover="this.style.textDecoration='underline';"
                        onmouseout="this.style.textDecoration='none';">
                        ${traderName}
                    </a>
                `;
                const priceDisplayHTML = `<span class="price-display">${formattedPrice}</span>`;

                bestBuyerInfoHTML = `
                    <span style="white-space:nowrap;">Best Trader: ${priceDisplayHTML}</span>
                    <span style="white-space:nowrap;">by ${traderLinkHTML}${upvoteText}${teListingsLinkHTML}</span>
                `;

            } else {
                // Fallback (trader name is not a link)
                const priceDisplayHTML = `<span class="price-display">${formattedPrice}</span>`;

                bestBuyerInfoHTML = `
                    <span style="white-space:nowrap;">Best Trader: ${priceDisplayHTML}</span>
                    <span style="white-space:nowrap;">by <span style="color:#1E90FF;">${traderName}</span>${upvoteText}${teListingsLinkHTML}</span>
                `;
            }
        } else if (bestBuyer && bestBuyer.price) {
            const formattedPrice = `$${Math.round(bestBuyer.price).toLocaleString()}`;
            const priceDisplayHTML = `<span class="price-display">${formattedPrice}</span>`;

            bestBuyerInfoHTML = `
                <span style="white-space:nowrap;">Best Trader: ${priceDisplayHTML}</span>
                ${teListingsLinkHTML}
            `;
        } else {
             bestBuyerInfoHTML = `${teListingsLinkHTML}`;
        }

        return bestBuyerInfoHTML;
    }

    function createInfoContainer(itemName, itemId, marketValue, bestBuyer, upvoteCount, traderId) {
        const container = document.createElement('div');
        container.className = 'bazaar-info-container'; // Class used instead of inline style
        container.dataset.itemid = itemId;
        if (marketValue) container.dataset.marketValue = marketValue;
        if (traderId) container.dataset.bestBuyerId = traderId;

        const marketText = marketValue ? ` <span class="bazaar-market-value">(Market Value: ${marketValue})</span>` : '';
        const encodedItemName = encodeURIComponent(itemName);

        const bestBuyerInfoHTML = createBestBuyerHTML(bestBuyer, upvoteCount, traderId, encodedItemName);

        const itemIdHTML = `
            <span class="bazaar-item-id">
                Item #: ${itemId}
            </span>
        `;

        const bestBuyerHTML = `
            <div class="best-buyer-line">
                ${bestBuyerInfoHTML}
                ${itemIdHTML}
            </div>
        `;

        // --- Filter and Sort Controls (using classes) ---
        const filterControlsHTML = `
            <div class="bazaar-control-row">
                <div class="bazaar-control-line">
                    <div class="bazaar-price-controls">
                        <button class="bazaar-filter-toggle-btn" data-filter-type="price">
                            Price
                        </button>
                        <div class="bazaar-sort-visuals">
                            <span class="bazaar-sort-btn" data-sort-by="price" data-sort-dir="asc">
                                🔼
                            </span>
                            <span class="bazaar-sort-btn" data-sort-by="price" data-sort-dir="desc">
                                🔽
                            </span>
                        </div>
                    </div>

                    <div class="bazaar-quantity-controls">
                        <button class="bazaar-filter-toggle-btn" data-filter-type="quantity">
                            Qty
                        </button>
                       <div class="bazaar-sort-visuals">
                            <span class="bazaar-sort-btn" data-sort-by="quantity" data-sort-dir="asc">
                                🔼
                            </span>
                            <span class="bazaar-sort-btn" data-sort-by="quantity" data-sort-dir="desc">
                                🔽
                            </span>
                        </div>
                    </div>

                    <div class="bazaar-filter-inputs-group">
                        <div class="bazaar-filter-group-price">
                            <input type="number" placeholder="Min Price" class="bazaar-filter-input" data-filter-type="minPrice">
                            <input type="number" placeholder="Max Price" class="bazaar-filter-input" data-filter-type="maxPrice">
                        </div>

                        <div class="bazaar-filter-group-quantity">
                            <input type="number" placeholder="Min Qty" class="bazaar-filter-input" data-filter-type="minQty">
                            <input type="number" placeholder="Max Qty" class="bazaar-filter-input" data-filter-type="maxQty">
                        </div>

                       <button class="bazaar-apply-btn">
                            Apply
                        </button>

                        <button class="bazaar-reset-all-btn" title="Reset Filters and Visited Links">
                            ↺
                        </button>
                    </div>
                </div>
            </div>
        `;
        // --- END Filter and Sort Controls ---

        container.innerHTML = `
            <div class="bazaar-info-header">
                <span class="bazaar-title">Bazaar Listings for ${itemName}${marketText}</span>
                <span class="bazaar-count-info"></span>
            </div>
            ${bestBuyerHTML}
            ${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;
    }
    // --- END createInfoContainer ---


    // --- Core Logic ---

    function sortAndFilterListings(itemId, container) {
        let listings = window._cachedListings[itemId];
        if (!listings) 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;
        const isFiltered = minPrice !== null || maxPrice !== null || minQty !== null || maxQty !== 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 {
                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 marketValueStr = container.dataset.marketValue;
        const marketNum = marketValueStr ? parseInt(marketValueStr.replace(/\D/g,'')) : null;
        renderCards(container, filteredListings, marketNum, isFiltered);
    }

    function resetAllState(container, itemId) {
        window._visitedBazaars.clear();
        container.querySelector('[data-filter-type="minPrice"]').value = '';
        container.querySelector('[data-filter-type="maxPrice"]').value = '';
        container.querySelector('[data-filter-type="minQty"]').value = '';
        container.querySelector('[data-filter-type="maxQty"]').value = '';
        sortAndFilterListings(itemId, container);
    }

    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 resetAllBtn = container.querySelector('.bazaar-reset-all-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 = activeColor;
                 } else {
                     btn.style.background = defaultColor;
                 }
             });

             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', () => {
                window._activeSort.type = btn.dataset.sortBy;
                window._activeSort.dir = btn.dataset.sortDir;
                updateSortVisuals();
                sortAndFilterListings(itemId, container);
            });
        });

        filterToggleBtns.forEach(btn => {
            btn.addEventListener('click', () => {
                const filterType = btn.dataset.filterType;
                const targetGroup = filterType === 'price' ? priceFilterGroup : quantityFilterGroup;
                const wasActive = btn.classList.contains('active-filter');

                priceFilterGroup.style.display = 'none';
                quantityFilterGroup.style.display = 'none';
                applyBtn.style.display = 'none';
                filterToggleBtns.forEach(b => b.classList.remove('active-filter'));

                if (!wasActive) {
                    targetGroup.style.display = 'flex';
                    applyBtn.style.display = 'block';
                    btn.classList.add('active-filter');
                }

                updateSortVisuals();
                sortAndFilterListings(itemId, container);
            });
        });

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

        if (resetAllBtn) {
            resetAllBtn.addEventListener('click', () => {
                resetAllState(container, itemId);
            });
        }

        updateSortVisuals();

        const initialPriceToggle = container.querySelector('.bazaar-filter-toggle-btn[data-filter-type="price"]');
        if (initialPriceToggle) {
             initialPriceToggle.style.background = activeColor;
             initialPriceToggle.classList.add('active-filter');
             priceFilterGroup.style.display = 'flex';
             applyBtn.style.display = 'block';
        }

        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));
        });
    }

    function renderCards(container, listings, marketNum, isFiltered){
        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 countSpan = container.querySelector('.bazaar-count-info');
        if (countSpan) {
            const totalListings = window._cachedListings[container.dataset.itemid].length;
            if (isFiltered && listings.length < totalListings) {
                 countSpan.innerHTML = `(Displaying ${listings.length} Listings - Filtered)`;
                 countSpan.style.color = '#FFA500';
            } else if (totalListings > 100) {
                 countSpan.innerHTML = `(Displaying 100+ Listings)`;
                 countSpan.style.color = 'orange';
             } else if (totalListings > 0) {
                 countSpan.innerHTML = `(Displaying ${totalListings} Listings)`;
                 countSpan.style.color = '#aaa';
             } else {
                 countSpan.textContent = '';
             }
        }

        const bestBuyerId = container.dataset.bestBuyerId;
        listings.forEach(listing=>{
            const card=document.createElement('div');
            card.className = 'bazaar-card'; // Class added
            card.dataset.playerId = listing.player_id;
            const isVisited=window._visitedBazaars.has(listing.player_id);
            const isBestBuyer = bestBuyerId && listing.player_id == bestBuyerId;
            if (isBestBuyer) card.classList.add('is-best-buyer');

            const bazaarLink = `https://www.torn.com/bazaar.php?userId=${listing.player_id}&highlightItem=${listing.item_id}#/`;
            
            // Note: The linkColor logic is handled by the new CSS rules using :link and :visited pseudo-classes,
            // but the initial link HTML structure is crucial for that.

            if(!isVisited){
                card.addEventListener('mouseenter', ()=>card.style.transform='scale(1.03)');
                card.addEventListener('mouseleave', ()=>card.style.transform='scale(1)');
            }

            const priceNum = parseFloat(listing.price.toString().replace(/,/g, ''));
            const formattedPrice = `$${Math.round(priceNum).toLocaleString()}`;

            let diffTextHTML = '';
            if(marketNum){
                const percent = ((priceNum - marketNum)/marketNum*100).toFixed(1);

                let diffClass = 'diff-text-neutral';
                if (percent < -0.5) { diffClass = 'diff-text-negative'; }
                else if (percent > 0.5) { diffClass = 'diff-text-positive'; }

                const sign = percent > 0 ? '+' : '';

                diffTextHTML = `<span class="${diffClass}">${sign}${percent}%</span>`;
            }

            card.innerHTML=`
                <a href="${bazaarLink}" target="_blank" data-linkclump="true" class="player-link">
                    ${listing.player_name || 'Unknown'}
                </a>

                <div class="price-info"><b>Price:</b> ${formattedPrice}</div>

                <div class="qty-diff-info">
                    <span style="white-space: nowrap;"><b>Qty:</b> ${listing.quantity}</span>
                    <span style="white: nowrap;">${diffTextHTML}</span>
                </div>
            `;

            card.addEventListener('click', (e)=>{
                const link = e.currentTarget.querySelector('a:first-child');
                if(listing.player_id && link){
                    window._visitedBazaars.add(listing.player_id);
                    // CSS handles the color change based on the visited state, but we manually trigger the link style for immediate visual feedback
                    link.style.color='#800080';
                }
            });

            cardContainer.appendChild(card);
        });
    }

    // --- MAIN EXECUTION FLOW ---

    async function updateInfoContainer(wrapper,itemId,itemName){
        let infoContainer=document.querySelector(`.bazaar-info-container[data-itemid="${itemId}"]`);

        if(!infoContainer){
            const { marketValue, bestBuyer, upvoteCount, traderId } = await fetchTornExchangeData(itemId);

            infoContainer = createInfoContainer(itemName, itemId, marketValue, bestBuyer, upvoteCount, traderId);
            wrapper.insertBefore(infoContainer, wrapper.firstChild);

            fetchBazaarListings(itemId, infoContainer);

        } else {
             if (window._cachedListings[itemId]) {
                 sortAndFilterListings(itemId, infoContainer);
             }
        }
    }

    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;
                    const countSpan = infoContainer.querySelector('.bazaar-count-info');
                    if (countSpan) {
                         if (listingsReceived > 100) {
                              countSpan.innerHTML = `(Displaying 100+ Listings)`;
                              countSpan.style.color = 'orange';
                         } else if (listingsReceived > 0) {
                              countSpan.innerHTML = `(Displaying ${listingsReceived} Listings)`;
                              countSpan.style.color = '#aaa';
                         } else {
                              countSpan.textContent = '';
                         }
                    }

                    if(!data || !data.listings || listingsReceived === 0){
                        renderMessage(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(`%c[BazaarScript Error] Failed to process Weaver API response for item ${itemId}:`, 'color: red; font-weight: bold;', e);
                    renderMessage(infoContainer,true);
                }
            },
            onerror:function(error){
                console.error(`%c[BazaarScript Error] GM_xmlhttpRequest failed for item ${itemId}:`, 'color: red; font-weight: bold;', error);
                renderMessage(infoContainer,true);
            }
        });
    }

    function processSellerWrapper(wrapper){
        if(!wrapper || wrapper.dataset.bazaarProcessed) return;
        const itemTile = wrapper.closest('[class*="itemTile"]') || wrapper.previousElementSibling;
        if(!itemTile) return;
        let nameEl = itemTile.querySelector('div[class*="name"]') || itemTile.querySelector('div');
        const btn = itemTile.querySelector('button[aria-controls*="itemInfo"]');
        if(!nameEl || !btn) return;
        const itemName = nameEl.textContent.trim();
        const idParts = btn.getAttribute('aria-controls').split('-');
        const itemId = idParts[idParts.length-1];
        wrapper.dataset.bazaarProcessed='true';
        updateInfoContainer(wrapper,itemId,itemName);
    }

    function processAllSellerWrappers(root=document.body){
        const sellerWrappers=root.querySelectorAll('[class*="sellerListWrapper"]');
        sellerWrappers.forEach(wrapper=>processSellerWrapper(wrapper));
    }

    const observer=new MutationObserver(()=>{ processAllSellerWrappers(); });
    observer.observe(document.body,{childList:true,subtree:true});
    processAllSellerWrappers();

})();

// --- Bazaar Page Green Highlight (Unchanged) ---
(function(){
    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'});
            }
            const itemDetailsContainer = document.querySelector('[aria-labelledby*="itemInfo"]');
            if (itemDetailsContainer) {
                const itemImg = itemDetailsContainer.querySelector(`img[src*="/images/items/${itemIdToHighlight}/"]`);
                if (itemImg) {
                    itemImg.closest('div')?.style.setProperty('outline','3px solid green','important');
                }
            }
        });
    });
    observer.observe(document.body,{childList:true,subtree:true});
})();