Bazaars in Item Market 2.0

Displays bazaar listings with sorting controls via TornPal

Από την 21/02/2025. Δείτε την τελευταία έκδοση.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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