Bazaar + TE Info Final (Integrated & Fixed)

Bazaar listing ( Weaver Site ) + TE Information PC VERSION

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name          Bazaar + TE Info Final (Integrated & Fixed)
// @namespace     https://weav3r.dev/
// @version       3.5.1
// @description   Bazaar listing ( Weaver Site ) + TE Information PC VERSION
// @author        WTV [3281931]
// @match         https://www.torn.com/*
// @grant         GM_xmlhttpRequest
// @grant         GM_addStyle
// @connect       weav3r.dev
// @connect       tornexchange.com
// @run-at        document-idle
// ==/UserScript==

(function() {
    'use strict';

    // Global State
    window._cachedListings = {};
    window._marketValueCache = {};
    window._currentMarketNetPrice = 0;

    // --- CSS ---
    GM_addStyle(`
        .bazaar-info-container { border: 1px solid #888; margin: 10px 0; padding: 5px; background: #222; color: #fff; border-radius: 4px; }
        .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; }
        .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; }
        
        /* 4-Box Filter Grid */
        .filter-grid { display: flex; gap: 6px; margin: 8px 0; padding: 5px; border-top: 1px dashed #444; border-bottom: 1px dashed #444; }
        .filter-grid input { flex: 1; background: #111; border: 1px solid #666; color: #00FF00; padding: 5px; border-radius: 3px; text-align: center; font-size: 12px; min-width: 0; }
        .bazaar-reset-all-btn { background: #444; color: white; border: none; padding: 0 10px; cursor: pointer; font-weight: bold; border-radius: 3px; font-size: 16px; }

        .bazaar-market-calc { display: flex; align-items: center; gap: 10px; padding: 8px 0; margin-top: 5px; }
        .bazaar-calc-label { font-weight: bold; color: #ddd; font-size: 14px; white-space: nowrap; }
        .bazaar-net-profit { font-weight: bold; color: limegreen; font-size: 14px; white-space: nowrap; }
        
        .bazaar-card-container { display: flex; overflow-x: auto; padding: 5px; gap: 5px; min-height: 80px; }
        .bazaar-card { border: 1px solid #444; background: #222; color: #eee; padding: 10px; margin: 2px; width: 125px; flex-shrink: 0; display: flex; flex-direction: column; font-size: 15px; gap: 3px; border-radius: 4px; }
        .bazaar-card a { font-weight: bold; text-decoration: none; color: #1E90FF; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        
        .diff-text-positive { color: red; font-weight: bold; }
        .diff-text-negative { color: limegreen; font-weight: bold; }
        .diff-text-neutral { color: gold; font-weight: bold; }
        
        .bazaar-loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; margin: 10px auto; }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
    `);

    // --- UTILITIES ---
    function cleanName(rawName) {
        return rawName.replace(/View Info|Buy Item|Content (collapsed|expanded)/g, '').split('$')[0].trim();
    }

    async function fetchTornExchangeData(itemId) {
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: "GET", url: `https://tornexchange.com/api/best_listing?item_id=${itemId}`,
                onload: (r) => {
                    try {
                        const d = JSON.parse(r.responseText);
                        if(d && d.status === 'success') {
                            window._marketValueCache[itemId] = d.data.te_price;
                            resolve(d.data);
                        } else resolve(null);
                    } catch(e) { resolve(null); }
                }
            });
        });
    }

    // --- HIGHLIGHT & SCROLL LOGIC ---
    const checkAndScroll = () => {
        const params = new URLSearchParams(window.location.search);
        const targetId = params.get("highlightItem");
        if (!targetId || !window.location.href.includes("bazaar.php")) return false;

        const targetImg = document.querySelector(`img[src*="/images/items/${targetId}/"]`);
        if (targetImg) {
            const itemBox = targetImg.closest('li') || targetImg.closest('[class*="itemTile"]') || targetImg.parentElement;
            if (itemBox) {
                itemBox.style.setProperty("outline", "4px solid #00FF00", "important");
                itemBox.style.setProperty("outline-offset", "2px", "important");
                itemBox.style.setProperty("background", "rgba(0, 255, 0, 0.1)", "important");
                itemBox.scrollIntoView({ behavior: "smooth", block: "center" });
                return true;
            }
        }
        return false;
    };

    // --- UI GENERATION ---
    function createInfoContainer(itemName, itemId, teData) {
        const container = document.createElement('div');
        container.className = 'bazaar-info-container';
        container.dataset.itemid = itemId;

        const cleaned = cleanName(itemName);
        const marketVal = teData?.te_price ? `$${Math.round(teData.te_price).toLocaleString()}` : 'N/A';
        const encodedName = encodeURIComponent(cleaned);
        const teLink = `https://tornexchange.com/listings?model_name_contains=${encodedName}&order_by=&status=`;

        container.innerHTML = `
            <div class="bazaar-info-header">
                <span class="bazaar-title">${cleaned} (Market Value: ${marketVal})</span>
            </div>
            <div class="best-buyer-line">
                ${teData?.price ? `<span>Best Trader: <span class="price-display">$${Math.round(teData.price).toLocaleString()}</span> by <a class="trader-link" href="https://www.torn.com/profiles.php?XID=${teData.trader_id}" target="_blank">${teData.trader}</a></span>` : ''}
                <a href="${teLink}" target="_blank" class="te-listings-link">(TE Listings)</a>
                <span class="bazaar-item-id">Item #: ${itemId}</span>
            </div>
            <div class="filter-grid">
                <input type="number" class="f-min-p" placeholder="Min $">
                <input type="number" class="f-max-p" placeholder="Max $">
                <input type="number" class="f-min-q" placeholder="Min Qty">
                <input type="number" class="f-max-q" placeholder="Max Qty">
                <button class="bazaar-reset-all-btn">↺</button>
            </div>
            <div class="bazaar-market-calc">
                <span class="bazaar-calc-label">Profit Calc Sell Price:</span>
                <input type="text" placeholder="Enter Price" class="profit-calc-input" style="width:100px; background:#111; border:1px solid #666; color:#fff; padding:4px;">
                <span class="bazaar-calc-label">Net (5%):</span>
                <span class="bazaar-net-profit profit-net-display">$0</span>
            </div>
            <div class="bazaar-card-container"><div class="bazaar-loader"></div></div>
        `;

        setupListeners(container, itemId);
        return container;
    }

    function setupListeners(container, itemId) {
        container.querySelectorAll('.filter-grid input').forEach(input => {
            input.addEventListener('input', () => renderCards(itemId, container));
        });

        const calcInput = container.querySelector(`.profit-calc-input`);
        const netSpan = container.querySelector(`.profit-net-display`);
        calcInput.addEventListener('input', (e) => {
            const val = e.target.value.replace(/[^\d]/g, '');
            e.target.value = val ? Number(val).toLocaleString() : '';
            window._currentMarketNetPrice = Math.floor(parseInt(val || 0) * 0.95);
            netSpan.textContent = `$${window._currentMarketNetPrice.toLocaleString()}`;
            renderCards(itemId, container);
        });

        container.querySelector('.bazaar-reset-all-btn').addEventListener('click', () => {
            container.querySelectorAll('input').forEach(i => i.value = '');
            window._currentMarketNetPrice = 0;
            renderCards(itemId, container);
        });
    }

    function renderCards(itemId, container) {
        const listings = window._cachedListings[itemId] || [];
        const cardBox = container.querySelector('.bazaar-card-container');
        
        const minP = parseInt(container.querySelector('.f-min-p').value || 0);
        const maxP = parseInt(container.querySelector('.f-max-p').value || Infinity);
        const minQ = parseInt(container.querySelector('.f-min-q').value || 0);
        const maxQ = parseInt(container.querySelector('.f-max-q').value || Infinity);
        
        const marketVal = window._marketValueCache[itemId] || 0;
        if (!cardBox) return;
        cardBox.innerHTML = '';

        let filtered = listings.filter(l => 
            l.price >= minP && l.price <= (maxP || Infinity) &&
            l.quantity >= minQ && l.quantity <= (maxQ || Infinity)
        ).sort((a, b) => a.price - b.price);

        filtered.forEach(l => {
            const card = document.createElement('div');
            card.className = 'bazaar-card';
            const diff = marketVal ? ((l.price - marketVal) / marketVal * 100).toFixed(1) : 0;
            const diffClass = diff < -0.5 ? 'diff-text-negative' : (diff > 0.5 ? 'diff-text-positive' : 'diff-text-neutral');

            let marginHTML = '';
            if (window._currentMarketNetPrice > 0) {
                const margin = ((window._currentMarketNetPrice - l.price) / window._currentMarketNetPrice * 100).toFixed(2);
                const marginClass = margin > 0.1 ? 'diff-text-negative' : 'diff-text-positive';
                marginHTML = `<div style="font-size:14px"><b>Margin:</b> <span class="${marginClass}">${margin}%</span></div>`;
            }

            card.innerHTML = `
                <a href="https://www.torn.com/bazaar.php?userId=${l.player_id}&highlightItem=${itemId}#/" target="_blank">${l.player_name}</a>
                <div><b>Price:</b> $${Math.round(l.price).toLocaleString()}</div>
                <div style="display:flex; justify-content:space-between"><span><b>Qty:</b> ${l.quantity}</span><span class="${diffClass}">${diff > 0 ? '+' : ''}${diff}%</span></div>
                ${marginHTML}
            `;
            cardBox.appendChild(card);
        });
    }

    // --- MAIN OBSERVER ---
    const observer = new MutationObserver(() => {
        // Highlight check
        checkAndScroll();

        // Bazaar Data injection
        document.querySelectorAll('[class*="sellerListWrapper"]').forEach(async w => {
            if (w.dataset.bazaarProcessed) return;
            w.dataset.bazaarProcessed = 'true';
            
            const tile = w.closest('[class*="itemTile"]') || w.previousElementSibling;
            const btn = tile?.querySelector('button[aria-controls*="itemInfo"]');
            if (!btn) return;

            const itemId = btn.getAttribute('aria-controls').split('-').pop();
            const itemName = tile.querySelector('div').textContent.trim();
            
            const teData = await fetchTornExchangeData(itemId);
            const container = createInfoContainer(itemName, itemId, teData);
            w.insertBefore(container, w.firstChild);

            GM_xmlhttpRequest({
                method: "GET", url: `https://weav3r.dev/api/marketplace/${itemId}`,
                onload: (r) => {
                    const data = JSON.parse(r.responseText);
                    window._cachedListings[itemId] = data.listings;
                    renderCards(itemId, container);
                }
            });
        });
    });

    observer.observe(document.body, { childList: true, subtree: true });
    setTimeout(checkAndScroll, 1000);

})();