Bazaar + TE Info Final (Integrated & Fixed)

Bazaar listing ( Weaver Site ) + TE Information PC VERSION

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например 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);

})();