Bezas Bazaar

Highlight Torn listings based on manually saved prices (Item Market + Bazaar), using both ID and Name

目前为 2025-04-09 提交的版本。查看 最新版本

// ==UserScript==
// @name         Bezas Bazaar
// @namespace    http://tampermonkey.net/
// @version      3.0.1
// @description  Highlight Torn listings based on manually saved prices (Item Market + Bazaar), using both ID and Name
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// @match        https://www.torn.com/bazaar.php*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    const normalize = str => str?.toLowerCase().replace(/\s+/g, '_');

    function getSavedPrice(id, name = null) {
        const idKey = `manual_price_${id}`;
        const nameKey = name ? `manual_price_${normalizeKey(name)}` : null;

        const byId = GM_getValue(idKey);
        const byName = nameKey ? GM_getValue(nameKey) : undefined;

        return byId !== undefined ? byId : byName;
    }

    function savePrice(id, name, value) {
        if (id) GM_setValue(`manual_price_${id}`, value);
        if (name) GM_setValue(`manual_price_${normalizeKey(name)}`, value);
    }

    function normalizeKey(key) {
        return key?.toLowerCase().trim().replace(/\s+/g, '_');
    }

    GM_addStyle(`
        .manual-highlight-good { background-color: #004d00 !important; color: white !important; }
        .manual-highlight-warning { background-color: #ffa500 !important; color: black !important; }
        .manual-highlight-bad { background-color: #8b0000 !important; color: white !important; }
        .manual-highlight-missing { background-color: #9370DB !important; color: white !important; }
        .manual-price-diff {
            margin-left: 6px; font-size: 12px; font-weight: bold;
            color: black; background-color: rgba(255,255,255,0.6);
            padding: 1px 4px; border-radius: 4px;
        }
        #manual-price-modal {
            position: fixed; top: 30%; left: 50%;
            transform: translate(-50%, -30%);
            background: #1e1e1e; color: white;
            padding: 20px; border: 2px solid #888;
            border-radius: 10px; z-index: 9999; display: none;
        }
        #manual-price-modal input {
            width: 100px; padding: 5px;
            font-size: 14px;
        }
        #manual-price-modal button {
            margin-left: 10px;
            padding: 5px 10px;
            background: #444;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        .manual-overlay {
            position: absolute;
            top: 0; left: 0;
            width: 100%; height: 100%;
            z-index: 0;
            border-radius: 8px;
            opacity: 0.6;
            pointer-events: none;
        }
        .itemTile___cbw7w, .item___GYCYJ {
            position: relative;
        }
        #manual-price-drag-handle {
    cursor: move;
    user-select: none;
}

    `);

    // Modal
    const modal = document.createElement("div");
    modal.id = "manual-price-modal";
    modal.innerHTML = `
    <div id="manual-price-drag-handle" style="margin-bottom: 10px; font-weight: bold; cursor: move;">⇅ Drag to move</div>
    <label>Manual price: $<input type="number" id="manual-price-input" /></label>
    <button id="manual-price-save">Save</button>
    <button id="manual-price-cancel">Cancel</button>
`;

    document.body.appendChild(modal);
    makeModalDraggable();

    let currentItemId = null;
    let currentItemName = null;

    document.getElementById("manual-price-save").onclick = () => {
        const val = parseInt(document.getElementById("manual-price-input").value);
        if (val > 0) {
            savePrice(currentItemId, currentItemName, val);
            modal.style.display = "none";
            highlightAll();
        }
    };

    document.getElementById("manual-price-cancel").onclick = () => modal.style.display = "none";

    function showModal(id, name) {
        currentItemId = id;
        currentItemName = name;
        document.getElementById("manual-price-input").value = getSavedPrice(id, name) || '';
        modal.style.display = "block";
    }

    function makeModalDraggable() {
    const modal = document.getElementById("manual-price-modal");
    const handle = document.getElementById("manual-price-drag-handle");

    let offsetX = 0, offsetY = 0, isDragging = false;

    handle.addEventListener("mousedown", (e) => {
        isDragging = true;
        offsetX = e.clientX - modal.offsetLeft;
        offsetY = e.clientY - modal.offsetTop;
        document.body.style.userSelect = "none";
    });

    document.addEventListener("mouseup", () => {
        isDragging = false;
        document.body.style.userSelect = "auto";
    });

    document.addEventListener("mousemove", (e) => {
        if (!isDragging) return;
        modal.style.left = `${e.clientX - offsetX}px`;
        modal.style.top = `${e.clientY - offsetY}px`;
        modal.style.right = "auto";
        modal.style.bottom = "auto";
        modal.style.transform = "none";
    });
}



    function addDiff(el, diff) {
        if (el.querySelector('.manual-price-diff')) return;
        const span = document.createElement('span');
        span.className = 'manual-price-diff';
        span.textContent = `(${diff > 0 ? '+' : ''}${diff.toFixed(1)}%)`;
        el.appendChild(span);
    }

    function applyHighlight(el, listed, saved, priceEl) {
    el.classList.remove('manual-highlight-good', 'manual-highlight-warning', 'manual-highlight-bad', 'manual-highlight-missing');
    el.style.setProperty('background-color', '', 'important');
    el.style.setProperty('color', '', 'important');

    if (saved === undefined) {
        el.classList.add('manual-highlight-missing');
        el.style.setProperty('background-color', '#9370DB', 'important');
        el.style.setProperty('color', '#fff', 'important');
    } else {
        const diff = ((listed - saved) / saved) * 100;

        if (diff < 0) { //Work on getting this customizable % chance, default -5
            el.classList.add('manual-highlight-good');
            el.style.setProperty('background-color', '#004d00', 'important');
            el.style.setProperty('color', '#fff', 'important');
        } else if (diff >= 0 && diff <= 3) {
            el.classList.add('manual-highlight-warning');
            el.style.setProperty('background-color', '#ffa500', 'important');
            el.style.setProperty('color', '#000', 'important');
        } else {
            el.classList.add('manual-highlight-bad');
            el.style.setProperty('background-color', '#8b0000', 'important');
            el.style.setProperty('color', '#fff', 'important');
        }

        if (priceEl) addDiff(priceEl, diff);
    }
}

    function highlightItemTiles() {
    document.querySelectorAll('.itemTile___cbw7w').forEach(tile => {
        const img = tile.querySelector('img.torn-item');
        const priceSpan = tile.querySelector('.priceAndTotal___eEVS7 span');

        if (!img || !priceSpan) return;

        const idMatch = img.src.match(/\/items\/(\d+)\//);
        const itemId = idMatch ? idMatch[1] : null;
        if (!itemId) return;

        // Use regex to extract the first $amount (ignoring anything in percent)
        const fullText = priceSpan.textContent || '';
        const match = fullText.match(/\$([\d,]+)/);
        const listed = match ? parseInt(match[1].replace(/,/g, '')) : NaN;

        const saved = getSavedPrice(itemId);

        console.log(`[Tile DEBUG] ID=${itemId}, Raw="${fullText}", Listed=${listed}, Saved=${saved}`);

        if (!isNaN(listed)) {
            applyHighlight(tile, listed, saved, priceSpan);
        }
    });
}




    function highlightSellerRows(itemId) {
        const rows = document.querySelectorAll('li[class*="rowWrapper___"]');
        const saved = getSavedPrice(itemId, null);
        rows.forEach(row => {
            const priceEl = row.querySelector('div[class*="price___"]');
            const match = priceEl?.textContent.match(/\$([\d,]+)/);
            if (!priceEl || !match) return;
            const price = parseInt(match[1].replace(/,/g, ''));
            applyHighlight(row, price, saved, priceEl);
        });
    }

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

    function setupBuyButtons() {
        document.querySelectorAll('.itemTile___cbw7w .actionButton___pb_Da').forEach(btn => {
            if (btn.dataset.bound === "true") return;
            btn.dataset.bound = "true";

            btn.addEventListener('click', () => {
                const container = btn.closest('.itemTile___cbw7w');
                const img = container?.querySelector('img.torn-item');
                const nameEl = container?.querySelector('.itemName___3tW7n');
                const match = img?.src?.match(/\/items\/(\d+)\//);
                const id = match ? match[1] : null;
                const name = nameEl?.textContent?.trim() || null;
                showModal(id, name);
                highlightSellerRows(id);
                watchSellerRows(id);
            });
        });
    }

    function highlightFullListings() {
        const table = document.querySelector('#fullListingsView table');
        if (!table || (!currentItemId && !currentItemName)) return;

        const saved = getSavedPrice(currentItemId, currentItemName);
        table.querySelectorAll('tr').forEach(row => {
            const priceEl = row.querySelector('td:first-child');
            const match = priceEl?.textContent.match(/\$([\d,]+)/);
            if (!match) return;
            const listed = parseInt(match[1].replace(/,/g, ''));
            applyHighlight(row, listed, saved, priceEl);
        });
    }

    function highlightBazaarPage() {
    document.querySelectorAll('.itemsContainner___tVzIR .item___GYCYJ').forEach(item => {
        const nameEl = item.querySelector('.description___Y2Nrl .name___B0RW3');
        const priceEl = item.querySelector('.description___Y2Nrl .price___dJqda');
        if (!nameEl || !priceEl) return;

        const itemName = nameEl.textContent.trim();
        const rawPrice = priceEl.textContent.match(/\$[\d,]+/);
const listed = rawPrice ? parseInt(rawPrice[0].replace(/[^\d]/g, '')) : NaN;

        const saved = getSavedPrice(null, itemName);

        console.log(`[Bazaar DEBUG] Name=${itemName}, Raw="${rawPrice}", Listed=${listed}, Saved=${saved}`);

        if (!isNaN(listed)) {
            applyHighlight(item, listed, saved, priceEl);
            console.log(`[Bazaar APPLY] ${itemName} → Listed=${listed}, Saved=${saved}, Diff=${saved ? (((listed - saved) / saved) * 100).toFixed(2) + '%' : 'N/A'}`);
        }

        const btn = item.querySelector('button[aria-label^="Buy:"]');
        if (btn && !btn.dataset.bound) {
            btn.dataset.bound = "true";
            btn.addEventListener('click', () => showModal(null, itemName));
        }
    });
}






    function highlightAll() {
        if (location.href.includes("ItemMarket")) {
            highlightItemTiles();
            setupBuyButtons();
            highlightFullListings();
        } else if (location.href.includes("bazaar.php")) {
            highlightBazaarPage();
        }
    }

    new MutationObserver(highlightAll).observe(document.body, {
        childList: true, subtree: true
    });

    window.addEventListener('keydown', e => {
        if (e.key === "Escape") modal.style.display = "none";
    });

    highlightAll();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址