Greasy Fork

Greasy Fork is available in English.

Bazaars in Item Market 2.0

Displays bazaar listings with sorting controls via TornPal

当前为 2025-02-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Bazaars in Item Market 2.0
// @namespace    http://tampermonkey.net/
// @version      0.3
// @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';

    const CACHE_DURATION_MS = 60000;
    let currentSortKey = "price";
    let currentSortOrder = "asc";

    // Cache helpers
    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) {}
    }

    // 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';
    }

    // Modal pop-out
    function openModal(url) {
        const originalBodyOverflow = document.body.style.overflow;
        document.body.style.overflow = 'hidden';

        const modalOverlay = document.createElement('div');
        modalOverlay.id = 'bazaar-modal-overlay';
        Object.assign(modalOverlay.style, {
            position: 'fixed',
            top: '0',
            left: '0',
            width: '100%',
            height: '100%',
            backgroundColor: 'rgba(0, 0, 0, 0.7)',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            zIndex: '10000',
            overflow: 'visible'
        });

        const modalContainer = document.createElement('div');
        modalContainer.id = 'bazaar-modal-container';
        Object.assign(modalContainer.style, {
            backgroundColor: '#fff',
            border: '8px solid #000',
            boxShadow: '0 0 10px rgba(0,0,0,0.5)',
            borderRadius: '8px',
            position: 'relative',
            resize: 'both'
        });

        // Load size from localStorage
        const savedSize = localStorage.getItem('bazaarModalSize');
        if (savedSize) {
            try {
                const { width, height } = JSON.parse(savedSize);
                modalContainer.style.width = (width < 200 ? '80%' : width + 'px');
                modalContainer.style.height = (height < 200 ? '80%' : height + 'px');
            } catch(e) {
                modalContainer.style.width = '80%';
                modalContainer.style.height = '80%';
            }
        } else {
            modalContainer.style.width = '80%';
            modalContainer.style.height = '80%';
        }

        // Close button
        const closeButton = document.createElement('button');
        closeButton.textContent = '×';
        Object.assign(closeButton.style, {
            position: 'absolute',
            top: '-20px',
            right: '-20px',
            width: '40px',
            height: '40px',
            backgroundColor: '#ff0000',
            color: '#fff',
            border: 'none',
            borderRadius: '50%',
            fontSize: '24px',
            cursor: 'pointer',
            boxShadow: '0 0 5px rgba(0,0,0,0.5)'
        });
        closeButton.addEventListener('click', () => {
            modalOverlay.remove();
            document.body.style.overflow = originalBodyOverflow;
        });

        // Close when clicking outside
        modalOverlay.addEventListener('click', (e) => {
            if (e.target === modalOverlay) {
                modalOverlay.remove();
                document.body.style.overflow = originalBodyOverflow;
            }
        });

        // Iframe
        const iframe = document.createElement('iframe');
        Object.assign(iframe.style, {
            width: '100%',
            height: '100%',
            border: 'none'
        });
        iframe.src = url;

        modalContainer.appendChild(closeButton);
        modalContainer.appendChild(iframe);
        modalOverlay.appendChild(modalContainer);
        document.body.appendChild(modalOverlay);

        // Save new size on resize
        if (window.ResizeObserver) {
            const resizeObserver = new ResizeObserver(entries => {
                for (let entry of entries) {
                    const { width, height } = entry.contentRect;
                    localStorage.setItem('bazaarModalSize', JSON.stringify({
                        width: Math.round(width),
                        height: Math.round(height)
                    }));
                }
            });
            resizeObserver.observe(modalContainer);
        }
    }

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

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

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

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

        const sortSelect = document.createElement('select');
        Object.assign(sortSelect.style, {
            padding: '2px',
            border: '1px solid #444',
            borderRadius: '2px',
            backgroundColor: '#1a1a1a',
            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');
        Object.assign(orderToggle.style, {
            padding: '2px 4px',
            border: '1px solid #444',
            borderRadius: '2px',
            backgroundColor: '#1a1a1a',
            color: '#fff',
            cursor: 'pointer'
        });
        orderToggle.textContent = (currentSortOrder === "asc") ? "Asc" : "Desc";
        sortControls.appendChild(orderToggle);

        container.appendChild(sortControls);

        // Listings row
        const scrollWrapper = document.createElement('div');
        Object.assign(scrollWrapper.style, {
            overflowX: 'auto',
            overflowY: 'hidden',
            height: '120px',
            whiteSpace: 'nowrap',
            paddingBottom: '3px'
        });

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

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

        // "Powered by TornPal" credit
        const poweredBy = document.createElement('div');
        poweredBy.style.fontSize = '10px';
        poweredBy.style.textAlign = 'right';
        poweredBy.style.marginTop = '6px';
        poweredBy.innerHTML = `
          <span style="color:#666;">Powered by </span>
          <a href="https://tornpal.com/" target="_blank" style="color:#aaa; text-decoration:underline;">
            TornPal
          </a>
        `;
        container.appendChild(poweredBy);

        // Sort 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;
    }

    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);
        });
    }

    // Card with "Player: {ID}" as a link, plus a custom SVG for the modal
    function createListingCard(listing) {
        const card = document.createElement('div');
        card.className = 'listing-card';
        Object.assign(card.style, {
            backgroundColor: '#1a1a1a',
            color: '#fff',
            border: '1px solid #444',
            borderRadius: '4px',
            padding: '8px',
            minWidth: '220px',
            fontSize: '14px',
            display: 'inline-block',
            boxSizing: 'border-box'
        });

        const linkContainer = document.createElement('div');
        Object.assign(linkContainer.style, {
            display: 'flex',
            alignItems: 'center',
            gap: '5px',
            marginBottom: '6px'
        });

        // Normal link to the bazaar page
        const playerLink = document.createElement('a');
        playerLink.href = `https://www.torn.com/bazaar.php?userId=${listing.player_id}#/`;
        playerLink.textContent = `Player: ${listing.player_id}`;
        Object.assign(playerLink.style, {
            fontWeight: 'bold',
            color: '#00aaff',
            textDecoration: 'underline'
        });
        linkContainer.appendChild(playerLink);

        // Replace with user-provided SVG
        const iconSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        iconSvg.setAttribute("viewBox", "0 0 512 512");
        iconSvg.setAttribute("width", "16");
        iconSvg.setAttribute("height", "16");
        iconSvg.style.cursor = "pointer";
        iconSvg.style.color = "#ffa500"; // distinct color
        iconSvg.title = "Open in modal";
        iconSvg.innerHTML = `
            <path d="M432 64L208 64c-8.8 0-16 7.2-16 16l0 16-64 0 0-16c0-44.2 35.8-80 80-80L432 0c44.2 0 80 35.8 80 80l0 224c0 44.2-35.8 80-80 80l-16 0 0-64 16 0c8.8 0 16-7.2 16-16l0-224c0-8.8-7.2-16-16-16zM0 192c0-35.3 28.7-64 64-64l256 0c35.3 0 64 28.7 64 64l0 256c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 192zm64 32c0 17.7 14.3 32 32 32l192 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L96 192c-17.7 0-32 14.3-32 32z"/>
        `;
        iconSvg.addEventListener('click', (e) => {
            e.preventDefault();
            openModal(playerLink.href);
        });
        linkContainer.appendChild(iconSvg);

        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');
        Object.assign(footnote.style, {
            fontSize: '11px',
            color: '#aaa',
            textAlign: 'right'
        });
        footnote.textContent = `Updated: ${getRelativeTime(listing.updated)}`;

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

    function updateInfoContainer(wrapper, itemId, itemName) {
        let infoContainer = document.querySelector(`#item-info-container[data-itemid="${itemId}"]`);
        if (!infoContainer) {
            infoContainer = createInfoContainer(itemName, itemId);
            wrapper.insertBefore(infoContainer, wrapper.firstChild);
        } else {
            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);
                    }
                } catch (e) {}
            },
            onerror: function(err) {}
        });
    }

    // Desktop aggregator
    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 aggregator
    function processMobileSellerList() {
        if (window.innerWidth >= 784) return;
        const sellerList = document.querySelector('ul.sellerList___e4C9_');
        if (!sellerList) {
            const existing = document.querySelector('#item-info-container');
            if (existing) existing.remove();
            return;
        }
        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 (document.querySelector(`#item-info-container[data-itemid="${itemId}"]`)) return;
        const infoContainer = createInfoContainer(itemName, itemId);
        sellerList.parentNode.insertBefore(infoContainer, sellerList);
        updateInfoContainer(infoContainer, itemId, itemName);
    }

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

    // Initial run
    processAllSellerWrappers();
    processMobileSellerList();

    // Observe changes
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            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);
                    }
                }
            });
            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 });
})();