Torn Bazaar Filter

Show only selected Bazaar items and display owned quantities from Museum

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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
    );
 
})();