Bazaars in Item Market 2.0

Displays bazaar listings with sorting controls via TornPal

目前为 2025-02-21 提交的版本。查看 最新版本

// ==UserScript==
// @name         Bazaars in Item Market 2.0
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Displays bazaar listings with sorting controls via TornPal
// @author       Weav3r [1853324]
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// @grant        GM_xmlhttpRequest
// @connect      tornpal.com
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Cache duration: 60 seconds
    const CACHE_DURATION_MS = 60000;

    // Global sort settings
    let currentSortKey = "price";   // "price", "quantity", or "updated"
    let currentSortOrder = "asc";   // "asc" or "desc"

    // Helpers: caching
    function getCache(itemId) {
        try {
            const key = "tornBazaarCache_" + itemId;
            const cached = localStorage.getItem(key);
            if (cached) {
                const payload = JSON.parse(cached);
                if (Date.now() - payload.timestamp < CACHE_DURATION_MS) {
                    return payload.data;
                }
            }
        } catch(e) {

        }
        return null;
    }

    function setCache(itemId, data) {
        try {
            const key = "tornBazaarCache_" + itemId;
            const payload = { timestamp: Date.now(), data: data };
            localStorage.setItem(key, JSON.stringify(payload));
        } catch(e) {
            // intentionally left blank
        }
    }

    // Helper: relative time
    function getRelativeTime(timestampSeconds) {
        const now = Date.now();
        const diffSec = Math.floor((now - timestampSeconds * 1000) / 1000);
        if (diffSec < 60) return diffSec + 's ago';
        if (diffSec < 3600) return Math.floor(diffSec / 60) + 'm ago';
        if (diffSec < 86400) return Math.floor(diffSec / 3600) + 'h ago';
        return Math.floor(diffSec / 86400) + 'd ago';
    }

    // Creates the info container
    function createInfoContainer(itemName, itemId) {
        const container = document.createElement('div');
        container.id = 'item-info-container';
        container.setAttribute('data-itemid', itemId);
        container.style.backgroundColor = '#2f2f2f';
        container.style.color = '#ccc';
        container.style.fontSize = '13px';
        container.style.border = '1px solid #444';
        container.style.borderRadius = '4px';
        container.style.margin = '5px 0';
        container.style.padding = '10px';
        container.style.display = 'flex';
        container.style.flexDirection = 'column';
        container.style.gap = '8px';

        // Header
        const header = document.createElement('div');
        header.className = 'info-header';
        header.style.fontSize = '16px';
        header.style.fontWeight = 'bold';
        header.style.color = '#fff';
        header.textContent = `Item: ${itemName} (ID: ${itemId})`;
        container.appendChild(header);

        // Sort controls
        const sortControls = document.createElement('div');
        sortControls.className = 'sort-controls';
        sortControls.style.display = 'flex';
        sortControls.style.alignItems = 'center';
        sortControls.style.gap = '5px';
        sortControls.style.fontSize = '12px';
        sortControls.style.padding = '5px';
        sortControls.style.backgroundColor = '#333';
        sortControls.style.borderRadius = '4px';

        const sortLabel = document.createElement('span');
        sortLabel.textContent = "Sort by:";
        sortControls.appendChild(sortLabel);

        const sortSelect = document.createElement('select');
        sortSelect.style.padding = '2px';
        sortSelect.style.border = '1px solid #444';
        sortSelect.style.borderRadius = '2px';
        sortSelect.style.backgroundColor = '#1a1a1a';
        sortSelect.style.color = '#fff';
        [
            { value: "price", text: "Price" },
            { value: "quantity", text: "Quantity" },
            { value: "updated", text: "Last Updated" }
        ].forEach(opt => {
            const option = document.createElement('option');
            option.value = opt.value;
            option.textContent = opt.text;
            sortSelect.appendChild(option);
        });
        sortSelect.value = currentSortKey;
        sortControls.appendChild(sortSelect);

        const orderToggle = document.createElement('button');
        orderToggle.style.padding = '2px 4px';
        orderToggle.style.border = '1px solid #444';
        orderToggle.style.borderRadius = '2px';
        orderToggle.style.backgroundColor = '#1a1a1a';
        orderToggle.style.color = '#fff';
        orderToggle.style.cursor = 'pointer';
        orderToggle.textContent = (currentSortOrder === "asc") ? "Asc" : "Desc";
        sortControls.appendChild(orderToggle);

        container.appendChild(sortControls);

        // Scrollable listings row
        const scrollWrapper = document.createElement('div');
        scrollWrapper.style.overflowX = 'auto';
        scrollWrapper.style.overflowY = 'hidden';
        scrollWrapper.style.height = '120px';
        scrollWrapper.style.whiteSpace = 'nowrap';
        scrollWrapper.style.paddingBottom = '3px';

        const cardContainer = document.createElement('div');
        cardContainer.className = 'card-container';
        cardContainer.style.display = 'inline-flex';
        cardContainer.style.flexWrap = 'nowrap';
        cardContainer.style.gap = '10px';

        scrollWrapper.appendChild(cardContainer);
        container.appendChild(scrollWrapper);

        // Sorting events
        sortSelect.addEventListener('change', () => {
            currentSortKey = sortSelect.value;
            if (container.filteredListings) {
                renderCards(container, container.filteredListings);
            }
        });
        orderToggle.addEventListener('click', () => {
            currentSortOrder = (currentSortOrder === "asc") ? "desc" : "asc";
            orderToggle.textContent = (currentSortOrder === "asc") ? "Asc" : "Desc";
            if (container.filteredListings) {
                renderCards(container, container.filteredListings);
            }
        });

        return container;
    }

    // Sort + render listing cards
    function renderCards(infoContainer, listings) {
        const sorted = listings.slice().sort((a, b) => {
            let diff = 0;
            if (currentSortKey === "price") diff = a.price - b.price;
            else if (currentSortKey === "quantity") diff = a.quantity - b.quantity;
            else if (currentSortKey === "updated") diff = a.updated - b.updated;
            return (currentSortOrder === "asc") ? diff : -diff;
        });
        const cardContainer = infoContainer.querySelector('.card-container');
        cardContainer.innerHTML = '';
        sorted.forEach(listing => {
            const card = createListingCard(listing);
            cardContainer.appendChild(card);
        });
    }

    // Create listing card
    function createListingCard(listing) {
        const card = document.createElement('div');
        card.className = 'listing-card';
        card.style.backgroundColor = '#1a1a1a';
        card.style.color = '#fff';
        card.style.border = '1px solid #444';
        card.style.borderRadius = '4px';
        card.style.padding = '8px';
        card.style.minWidth = '200px';
        card.style.fontSize = '14px';
        card.style.display = 'inline-block';
        card.style.boxSizing = 'border-box';

        const playerLink = document.createElement('a');
        playerLink.href = `https://www.torn.com/bazaar.php?userId=${listing.player_id}#/`;
        playerLink.target = '_blank';
        playerLink.textContent = `Player: ${listing.player_id}`;
        playerLink.style.display = 'block';
        playerLink.style.fontWeight = 'bold';
        playerLink.style.color = '#00aaff';
        playerLink.style.textDecoration = 'underline';
        playerLink.style.marginBottom = '6px';

        const details = document.createElement('div');
        details.innerHTML = `
            <div><strong>Price:</strong> $${listing.price.toLocaleString()}</div>
            <div><strong>Qty:</strong> ${listing.quantity}</div>
        `;
        details.style.marginBottom = '6px';

        const footnote = document.createElement('div');
        footnote.style.fontSize = '11px';
        footnote.style.color = '#aaa';
        footnote.style.textAlign = 'right';
        footnote.textContent = `Updated: ${getRelativeTime(listing.updated)}`;

        card.appendChild(playerLink);
        card.appendChild(details);
        card.appendChild(footnote);
        return card;
    }

    // Fetch data + update container
    function updateInfoContainer(wrapper, itemId, itemName) {
        // Check globally for an existing container
        let infoContainer = document.querySelector(`#item-info-container[data-itemid="${itemId}"]`);
        if (!infoContainer) {
            infoContainer = createInfoContainer(itemName, itemId);
            wrapper.insertBefore(infoContainer, wrapper.firstChild);
        } else {
            // Update header if container exists
            const header = infoContainer.querySelector('.info-header');
            if (header) header.textContent = `Item: ${itemName} (ID: ${itemId})`;
            const cardContainer = infoContainer.querySelector('.card-container');
            if (cardContainer) cardContainer.innerHTML = '';
        }

        const cachedData = getCache(itemId);
        if (cachedData) {
            infoContainer.filteredListings = cachedData.listings;
            renderCards(infoContainer, cachedData.listings);
            return;
        }

        const url = `https://tornpal.com/api/v1/markets/clist/${itemId}`;
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.listings && Array.isArray(data.listings)) {
                        const filtered = data.listings.filter(l => l.source === "bazaar");
                        setCache(itemId, { listings: filtered });
                        infoContainer.filteredListings = filtered;
                        renderCards(infoContainer, filtered);
                    } else {

                    }
                } catch (e) {

                }
            },
            onerror: function(err) {

            }
        });
    }

    // Desktop: process each sellerListWrapper
    function processSellerWrapper(wrapper) {
        if (!wrapper || wrapper.id === 'item-info-container') return;
        const itemTile = wrapper.previousElementSibling;
        if (!itemTile) return;
        const nameEl = itemTile.querySelector('.name___ukdHN');
        const btn = itemTile.querySelector('button[aria-controls^="wai-itemInfo-"]');
        if (nameEl && btn) {
            const itemName = nameEl.textContent.trim();
            const idParts = btn.getAttribute('aria-controls').split('-');
            const itemId = idParts[idParts.length - 1];
            updateInfoContainer(wrapper, itemId, itemName);
        }
    }

    // Mobile: handle the seller list
    function processMobileSellerList() {
        if (window.innerWidth >= 784) return; // only mobile
        const sellerList = document.querySelector('ul.sellerList___e4C9_');

        // If no seller rows, remove container if present
        if (!sellerList) {
            const existing = document.querySelector('#item-info-container');
            if (existing) existing.remove();
            return;
        }

        // If we already created a container for this item, skip
        const headerEl = document.querySelector('.itemsHeader___ZTO9r .title___ruNCT');
        const itemName = headerEl ? headerEl.textContent.trim() : "Unknown";

        const btn = document.querySelector('.itemsHeader___ZTO9r button[aria-controls^="wai-itemInfo-"]');
        let itemId = "unknown";
        if (btn) {
            const parts = btn.getAttribute('aria-controls').split('-');
            itemId = (parts.length > 2) ? parts[parts.length - 2] : parts[parts.length - 1];
        }

        // If container for this item ID exists, skip
        if (document.querySelector(`#item-info-container[data-itemid="${itemId}"]`)) return;

        // Create + fetch
        const infoContainer = createInfoContainer(itemName, itemId);
        sellerList.parentNode.insertBefore(infoContainer, sellerList);
        updateInfoContainer(infoContainer, itemId, itemName);
    }

    // Desktop aggregator
    function processAllSellerWrappers(root = document.body) {
        // Skip on mobile
        if (window.innerWidth < 784) return;
        const wrappers = root.querySelectorAll('[class*="sellerListWrapper"]');
        wrappers.forEach(wrapper => processSellerWrapper(wrapper));
    }

    processAllSellerWrappers();
    processMobileSellerList();

    // Observe changes (both added and removed nodes)
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            // Handle added nodes
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    if (window.innerWidth < 784) {
                        if (node.matches('ul.sellerList___e4C9_')) {
                            processMobileSellerList();
                        }
                    } else {
                        if (node.matches('[class*="sellerListWrapper"]')) {
                            processSellerWrapper(node);
                        }
                        processAllSellerWrappers(node);
                    }
                }
            });
            // Handle removed nodes (remove container if sellerList goes away on mobile)
            mutation.removedNodes.forEach(node => {
                if (node.nodeType === Node.ELEMENT_NODE && node.matches('ul.sellerList___e4C9_')) {
                    if (window.innerWidth < 784) {
                        const container = document.querySelector('#item-info-container');
                        if (container) container.remove();
                    }
                }
            });
        });
    });

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