Torn Bazaar Filter

Show only selected Bazaar items and display owned quantities from Museum

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Torn Bazaar Filter
// @namespace    torn-bazaar-filter
// @version      1.3.2
// @description  Show only selected Bazaar items and display owned quantities from Museum
// @match        https://www.torn.com/bazaar*
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// ==/UserScript==
 
(function () {
    'use strict';
 
    /************************************************************
     * CONFIGURATION
     ************************************************************/
 
    const FILTER_ITEMS = [
        { id: 187, name: 'Teddy Bear' },
        { id: 186, name: 'Sheep' },
        { id: 215, name: 'Kitten' },
        { id: 261, name: 'Wolverine' },
        { id: 618, name: 'Stingray' },
        { id: 258, name: 'Jaguar' },
        { id: 273, name: 'Chamois' },
        { id: 268, name: 'Red Fox' },
        { id: 266, name: 'Nessie' },
        { id: 269, name: 'Monkey' },
        { id: 274, name: 'Panda' },
        { id: 281, name: 'Lion' },
        { id: 384, name: 'Camel' },
    ];
 
    const FILTER_SET = new Set(
        FILTER_ITEMS.map(item => item.id)
    );
 
 
    /************************************************************
     * PERSISTENT STATE
     ************************************************************/
 
    const STORAGE_MINIMIZED =
        'tbf-panel-minimized';
 
    const STORAGE_FILTER_ENABLED =
        'tbf-filter-enabled';
 
 
    let filterEnabled =
        localStorage.getItem(
            STORAGE_FILTER_ENABLED
        ) !== 'false';
 
    let panelMinimized =
        localStorage.getItem(
            STORAGE_MINIMIZED
        ) === 'true';
 
 
    /************************************************************
     * RUNTIME STATE
     ************************************************************/
 
    let panel = null;
 
    let observer = null;
 
    let filterTimeout = null;
 
    let museumIframe = null;
 
    // itemId -> { amount, name }
    const inventory = new Map();
 
 
    /************************************************************
     * INITIALIZE INVENTORY
     *
     * Every configured item starts at 0.
     * Museum results overwrite the ones actually owned.
     ************************************************************/
 
    function initializeInventory() {
 
        inventory.clear();
 
        for (const item of FILTER_ITEMS) {
 
            inventory.set(item.id, {
                amount: 0,
                name: item.name
            });
        }
    }
 
 
    /************************************************************
     * LOAD MUSEUM
     *
     * Uses a hidden iframe so Torn's own JavaScript can run.
     ************************************************************/
 
    function loadMuseumInventory() {
 
        initializeInventory();
 
        console.log(
            '[TBF] Loading Museum...'
        );
 
 
        // Remove previous iframe if one exists.
        if (museumIframe) {
 
            try {
                museumIframe.remove();
            } catch (e) {
                // Ignore
            }
 
            museumIframe = null;
        }
 
 
        const iframe =
            document.createElement('iframe');
 
        museumIframe = iframe;
 
        iframe.src =
            'https://www.torn.com/museum.php';
 
 
        Object.assign(
            iframe.style,
            {
                position: 'fixed',
                width: '1px',
                height: '1px',
                left: '-10000px',
                top: '-10000px',
                opacity: '0',
                pointerEvents: 'none',
                border: '0'
            }
        );
 
 
        document.body.appendChild(iframe);
 
 
        iframe.addEventListener(
            'load',
            function () {
 
                console.log(
                    '[TBF] Museum iframe loaded'
                );
 
 
                /*
                 * Museum content may be populated
                 * asynchronously by Torn.
                 *
                 * Check every 500ms for up to 10 seconds.
                 */
 
                let attempts = 0;
 
                const checkMuseum =
                    setInterval(
                        function () {
 
                            attempts++;
 
                            try {
 
                                const museumDocument =
                                    iframe.contentDocument ||
                                    iframe.contentWindow.document;
 
 
                                const museumItems =
                                    museumDocument.querySelectorAll(
                                        '.item-wrapper[itemid]'
                                    );
 
 
                                console.log(
                                    `[TBF] Museum check ${attempts}: ${museumItems.length} items`
                                );
 
 
                                if (
                                    museumItems.length > 0 ||
                                    attempts >= 20
                                ) {
 
                                    clearInterval(
                                        checkMuseum
                                    );
 
 
                                    processMuseum(
                                        museumDocument
                                    );
 
 
                                    /*
                                     * We don't need the iframe anymore.
                                     */
                                    setTimeout(
                                        () => {
 
                                            if (
                                                museumIframe === iframe
                                            ) {
 
                                                iframe.remove();
 
                                                museumIframe =
                                                    null;
                                            }
 
                                        },
                                        100
                                    );
                                }
 
 
                            } catch (error) {
 
                                console.error(
                                    '[TBF] Error reading Museum:',
                                    error
                                );
 
                                if (
                                    attempts >= 20
                                ) {
 
                                    clearInterval(
                                        checkMuseum
                                    );
                                }
                            }
 
                        },
                        500
                    );
            }
        );
    }
 
 
    /************************************************************
     * PROCESS MUSEUM DOM
     ************************************************************/
 
    function processMuseum(
        museumDocument
    ) {
 
        console.log(
            '[TBF] Processing Museum...'
        );
 
 
        const museumItems =
            museumDocument.querySelectorAll(
                '.item-wrapper[itemid]'
            );
 
 
        console.log(
            `[TBF] Found ${museumItems.length} Museum items`
        );
 
 
        for (
            const element of museumItems
        ) {
 
            const itemId =
                Number(
                    element.getAttribute(
                        'itemid'
                    )
                );
 
 
            /*
             * Ignore items not in our filter list.
             */
 
            if (
                !FILTER_SET.has(itemId)
            ) {
                continue;
            }
 
 
            const amountElement =
                element.querySelector(
                    '.item-amount.qty'
                );
 
 
            const nameElement =
                element.querySelector(
                    '.coll-item-header'
                );
 
 
            let amount = 0;
 
 
            if (amountElement) {
 
                amount =
                    parseInt(
                        amountElement
                            .textContent
                            .trim()
                            .replace(/,/g, ''),
                        10
                    ) || 0;
            }
 
 
            const configuredItem =
                FILTER_ITEMS.find(
                    item =>
                        item.id === itemId
                );
 
 
            const name = configuredItem
                        ? configuredItem.name
                        : `Item ${itemId}`;
 
 
            inventory.set(
                itemId,
                {
                    amount,
                    name
                }
            );
 
 
            console.log(
                `[TBF] ${name}: ${amount}`
            );
        }
 
 
        console.log(
            '[TBF] Final inventory:',
            Object.fromEntries(
                inventory
            )
        );
 
 
        /*
         * Update Bazaar.
         */
 
        updateOwnedLabels();
 
        updatePanel();
    }
 
 
    /************************************************************
     * GET ITEM ID FROM BAZAAR CARD
     ************************************************************/
 
    function getItemId(card) {
 
        const image =
            card.querySelector(
                'img.item-plate'
            );
 
 
        if (!image) {
            return null;
        }
 
 
        const src =
            image.getAttribute('src') ||
            image.getAttribute('srcset');
 
 
        if (!src) {
            return null;
        }
 
 
        /*
         * Example:
         *
         * /images/items/258/large.png
         *
         * => 258
         */
 
        const match =
            src.match(
                /\/images\/items\/(\d+)\//
            );
 
 
        return match
            ? Number(match[1])
            : null;
    }
 
 
    /************************************************************
     * FIND BAZAAR CARDS
     ************************************************************/
 
    function getBazaarCards() {
 
        return Array.from(
            document.querySelectorAll(
                '[data-testid="item-description"]'
            )
        );
    }
 
 
    /************************************************************
     * APPLY FILTER
     ************************************************************/
 
    function applyFilter() {
 
        const cards =
            getBazaarCards();
 
 
        let shown = 0;
 
        let hidden = 0;
 
 
        for (
            const card of cards
        ) {
 
            const itemId =
                getItemId(card);
 
 
            /*
             * If we can't identify it,
             * don't touch it.
             */
 
            if (
                itemId === null
            ) {
 
                card.style.display =
                    '';
 
                continue;
            }
 
 
            /*
             * Filter OFF
             */
 
            if (
                !filterEnabled
            ) {
 
                card.style.display =
                    '';
 
                removeOwnedLabel(
                    card
                );
 
                continue;
            }
 
 
            /*
             * Filter ON:
             *
             * Only configured items
             * remain visible.
             */
 
            if (
                FILTER_SET.has(itemId)
            ) {
 
                card.style.display =
                    '';
 
                addOwnedLabel(
                    card,
                    itemId
                );
 
                shown++;
 
            } else {
 
                card.style.display =
                    'none';
 
                hidden++;
            }
        }
 
 
        updatePanel(
            shown,
            hidden
        );
    }
 
 
    /************************************************************
     * ADD OWNED LABEL
     ************************************************************/
 
    function addOwnedLabel(
        card,
        itemId
    ) {
 
        let label =
            card.querySelector(
                '.tbf-owned'
            );
 
 
        if (!label) {
 
            label =
                document.createElement(
                    'div'
                );
 
            label.className =
                'tbf-owned';
 
 
            Object.assign(
                label.style,
                {
                    marginTop: '4px',
                    fontSize: '12px',
                    fontWeight: 'bold',
                    textAlign: 'center',
                    opacity: '0.9'
                }
            );
 
 
            const description =
                card.querySelector(
                    '[data-testid="description"]'
                );
 
 
            if (description) {
 
                description.appendChild(
                    label
                );
 
            } else {
 
                card.appendChild(
                    label
                );
            }
        }
 
 
        const item =
            inventory.get(
                itemId
            );
 
 
        const amount =
            item
                ? item.amount
                : 0;
 
 
        label.textContent =
            `You own: ${amount}`;
    }
 
 
    /************************************************************
     * REMOVE OWNED LABEL
     ************************************************************/
 
    function removeOwnedLabel(
        card
    ) {
 
        const label =
            card.querySelector(
                '.tbf-owned'
            );
 
 
        if (label) {
            label.remove();
        }
    }
 
 
    /************************************************************
     * UPDATE OWNED LABELS
     ************************************************************/
 
    function updateOwnedLabels() {
 
        if (!filterEnabled) {
            return;
        }
 
 
        for (
            const card of getBazaarCards()
        ) {
 
            const itemId =
                getItemId(card);
 
 
            if (
                itemId !== null &&
                FILTER_SET.has(itemId)
            ) {
 
                addOwnedLabel(
                    card,
                    itemId
                );
            }
        }
    }
 
 
    /************************************************************
     * CREATE PANEL
     ************************************************************/
 
    function createPanel() {
 
        /*
         * Don't create duplicates.
         */
 
        if (
            panel &&
            document.body.contains(panel)
        ) {
            return;
        }
 
 
        panel =
            document.createElement(
                'div'
            );
 
 
        panel.id =
            'tbf-panel';
 
 
        Object.assign(
            panel.style,
            {
                position: 'fixed',
                top: '120px',
                right: '20px',
                zIndex: '99999',
                background: 'rgba(20,20,20,0.95)',
                color: '#fff',
                padding: '10px 12px',
                borderRadius: '8px',
                boxShadow:
                    '0 2px 10px rgba(0,0,0,0.4)',
                fontFamily:
                    'Arial,sans-serif',
                fontSize: '13px',
                minWidth: '190px'
            }
        );
 
 
        document.body.appendChild(
            panel
        );
 
 
        /*
         * Restore previous state.
         */
 
        if (
            panelMinimized
        ) {
 
            renderMinimizedPanel();
 
        } else {
 
            renderFullPanel();
        }
    }
 
 
    /************************************************************
     * FULL PANEL
     ************************************************************/
 
    function renderFullPanel() {
 
        if (!panel) {
            return;
        }
 
 
        Object.assign(
            panel.style,
            {
                minWidth: '190px',
                width: '',
                height: '',
                padding: '10px 12px',
                borderRadius: '8px',
                display: 'block'
            }
        );
 
 
        panel.innerHTML = `
 
            <div style="
                display:flex;
                justify-content:space-between;
                align-items:center;
                gap:10px;
                margin-bottom:8px;
            ">
 
                <strong>
                    Bazaar Filter
                </strong>
 
 
                <div style="
                    display:flex;
                    gap:4px;
                ">
 
                    <button
                        id="tbf-refresh"
                        type="button"
                        title="Refresh Museum inventory"
                        style="
                            border:0;
                            background:transparent;
                            color:#fff;
                            cursor:pointer;
                            font-size:15px;
                            padding:2px 4px;
                        "
                    >↻</button>
 
 
                    <button
                        id="tbf-minimize"
                        type="button"
                        title="Minimize"
                        style="
                            border:0;
                            background:transparent;
                            color:#fff;
                            cursor:pointer;
                            font-size:16px;
                            padding:2px 4px;
                        "
                    >−</button>
 
                </div>
 
            </div>
 
 
            <div style="
                display:flex;
                justify-content:space-between;
                align-items:center;
                margin-bottom:8px;
            ">
 
                <span id="tbf-status">
                </span>
 
 
                <button
                    id="tbf-toggle"
                    type="button"
                    style="
                        border:0;
                        border-radius:12px;
                        padding:3px 10px;
                        cursor:pointer;
                        font-weight:bold;
                    "
                ></button>
 
            </div>
 
 
            <div id="tbf-items">
            </div>
        `;
 
 
        /*
         * Minimize
         */
 
        panel
            .querySelector(
                '#tbf-minimize'
            )
            .addEventListener(
                'click',
                minimizePanel
            );
 
 
        /*
         * Refresh
         */
 
        panel
            .querySelector(
                '#tbf-refresh'
            )
            .addEventListener(
                'click',
                loadMuseumInventory
            );
 
 
        /*
         * Toggle
         */
 
        panel
            .querySelector(
                '#tbf-toggle'
            )
            .addEventListener(
                'click',
                () => {
 
                    filterEnabled =
                        !filterEnabled;
 
 
                    localStorage.setItem(
                        STORAGE_FILTER_ENABLED,
                        filterEnabled
                    );
 
 
                    updateToggle();
 
                    applyFilter();
                }
            );
 
 
        updateToggle();
 
        updatePanel();
    }
 
 
    /************************************************************
     * MINIMIZED PANEL
     ************************************************************/
 
    function renderMinimizedPanel() {
 
        if (!panel) {
            return;
        }
 
 
        Object.assign(
            panel.style,
            {
                minWidth: '0',
                width: '32px',
                height: '32px',
                padding: '4px',
                borderRadius: '50%',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center'
            }
        );
 
 
        panel.innerHTML = `
 
            <button
                id="tbf-restore"
                type="button"
                title="Open Bazaar Filter"
                style="
                    border:0;
                    background:transparent;
                    color:#fff;
                    cursor:pointer;
                    font-size:18px;
                    font-weight:bold;
                    padding:2px 5px;
                "
            >☰</button>
        `;
 
 
        panel
            .querySelector(
                '#tbf-restore'
            )
            .addEventListener(
                'click',
                restorePanel
            );
    }
 
 
    /************************************************************
     * MINIMIZE
     ************************************************************/
 
    function minimizePanel() {
 
        if (!panel) {
            return;
        }
 
 
        panelMinimized =
            true;
 
 
        localStorage.setItem(
            STORAGE_MINIMIZED,
            'true'
        );
 
 
        renderMinimizedPanel();
    }
 
 
    /************************************************************
     * RESTORE
     ************************************************************/
 
    function restorePanel() {
 
        if (!panel) {
            return;
        }
 
 
        panelMinimized =
            false;
 
 
        localStorage.setItem(
            STORAGE_MINIMIZED,
            'false'
        );
 
 
        renderFullPanel();
 
 
        /*
         * Opening the panel refreshes
         * the Museum quantities.
         */
 
        loadMuseumInventory();
    }
 
 
    /************************************************************
     * TOGGLE BUTTON
     ************************************************************/
 
    function updateToggle() {
 
        if (!panel) {
            return;
        }
 
 
        const toggle =
            panel.querySelector(
                '#tbf-toggle'
            );
 
 
        if (!toggle) {
            return;
        }
 
 
        toggle.textContent =
            filterEnabled
                ? 'ON'
                : 'OFF';
 
 
        if (
            filterEnabled
        ) {
 
            toggle.style.background =
                '#4caf50';
 
            toggle.style.color =
                '#fff';
 
        } else {
 
            toggle.style.background =
                '#777';
 
            toggle.style.color =
                '#fff';
        }
    }
 
 
    /************************************************************
     * PANEL CONTENT
     ************************************************************/
 
    function updatePanel(
        shown = 0,
        hidden = 0
    ) {
 
        if (
            !panel ||
            panelMinimized
        ) {
            return;
        }
 
 
        const status =
            panel.querySelector(
                '#tbf-status'
            );
 
 
        const itemsContainer =
            panel.querySelector(
                '#tbf-items'
            );
 
 
        if (
            !status ||
            !itemsContainer
        ) {
            return;
        }
 
 
        if (
            !filterEnabled
        ) {
 
            status.textContent =
                'Filter disabled';
 
            itemsContainer.innerHTML =
                '';
 
            return;
        }
 
 
        status.textContent =
            `${shown} selected item${shown === 1 ? '' : 's'} shown`;
 
 
        itemsContainer.innerHTML =
            '';
 
 
        for (
            const config of FILTER_ITEMS
        ) {
 
            const item =
                inventory.get(
                    config.id
                );
 
 
            const row =
                document.createElement(
                    'div'
                );
 
 
            Object.assign(
                row.style,
                {
                    display: 'flex',
                    justifyContent:
                        'space-between',
                    gap: '15px',
                    padding: '2px 0'
                }
            );
 
 
            const name =
                item
                    ? item.name
                    : config.name;
 
 
            const amount =
                item
                    ? item.amount
                    : 0;
 
 
            row.innerHTML = `
                <span>
                    ${escapeHtml(name)}
                </span>
 
                <strong>
                    ${amount}
                </strong>
            `;
 
 
            itemsContainer.appendChild(
                row
            );
        }
    }
 
 
    /************************************************************
     * HTML ESCAPE
     ************************************************************/
 
    function escapeHtml(
        text
    ) {
 
        const div =
            document.createElement(
                'div'
            );
 
 
        div.textContent =
            text;
 
 
        return div.innerHTML;
    }
 
 
    /************************************************************
     * OBSERVER
     *
     * Torn can replace parts/all of the page when navigating.
     ************************************************************/
 
    function setupObserver() {
 
        if (observer) {
            return;
        }
 
 
        observer =
            new MutationObserver(
                function () {
 
                    clearTimeout(
                        filterTimeout
                    );
 
 
                    filterTimeout =
                        setTimeout(
                            function () {
 
                                /*
                                 * If Torn removed our panel,
                                 * recreate it.
                                 */
 
                                if (
                                    !document.body.contains(
                                        panel
                                    )
                                ) {
 
                                    panel = null;
 
                                    createPanel();
                                }
 
 
                                /*
                                 * Reapply Bazaar filter.
                                 */
 
                                applyFilter();
 
                            },
                            100
                        );
                }
            );
 
 
        observer.observe(
            document.body,
            {
                childList: true,
                subtree: true
            }
        );
    }
 
 
    /************************************************************
     * INITIALIZATION
     ************************************************************/
 
    function init() {
 
        console.log(
            '[TBF] Initializing Bazaar Filter'
        );
 
 
        createPanel();
 
 
        /*
         * Load current Museum quantities.
         */
 
        loadMuseumInventory();
 
 
        /*
         * Filter current Bazaar.
         */
 
        applyFilter();
 
 
        /*
         * Watch for Torn page changes.
         */
 
        setupObserver();
    }
 
 
    /*
     * Wait for Torn to render.
     */
 
    setTimeout(
        init,
        1000
    );
 
})();