Bazaar Item Search powered by IronNerd

View items you are searching for in bazaars!

目前為 2025-03-18 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Bazaar Item Search powered by IronNerd
// @namespace    [email protected]
// @version      0.5.1
// @description  View items you are searching for in bazaars!
// @author       Nurv [669537]
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-end
// @license      Copyright IronNerd.me
// @connect      ironnerd.me
// ==/UserScript==

(function () {
    'use strict';

    const BACKEND_URL = 'https://www.ironnerd.me';
    const ongoingRequests = new Set();
    let allBazaarItems = [];
    let currentItemData = null;
    let lastUrl = location.href;
    let sortKey = 'price';
    let sortOrder = 'asc';

    function init() {
        injectAdditionalStyles();
        ensureBazaarEnhancerContainer().then(container => {
            initTabbedInterface(container);
            observeDarkMode();
            const info = getItemInfoFromURL();
            if (info.itemID) {
                currentItemData = info;
                fetchBazaarItems(info.itemID);
            } else {
                clearListingsData();
            }
            adjustBazaarEnhancerContainerTheme();
        });
    }

    function getItemInfoFromURL() {
        const url = new URL(window.location.href);
        let itemID = null;
        let itemName = '';
        if (url.hash) {
            let hash = url.hash.startsWith("#/") ? url.hash.substring(2) : url.hash.substring(1);
            let params = new URLSearchParams(hash);
            itemID = params.get("itemID");
            itemName = decodeURIComponent(params.get("itemName") ?? "");

        }
        if (!itemID) {
            let params = url.searchParams;
            itemID = params.get("itemID");
            itemName = decodeURIComponent(params.get("itemName") ?? "");
        }
        return {
            itemID: itemID ? parseInt(itemID, 10) : null,
            itemName: itemName
        };
    }

    function clearListingsData() {
        const topCheapestView = document.getElementById('topCheapestView');
        const fullListingsView = document.getElementById('fullListingsView');
        if (topCheapestView) {
            topCheapestView.innerHTML = `<p>No item selected.</p>`;
        }
        if (fullListingsView) {
            fullListingsView.innerHTML = `<p>No item selected.</p>`;
        }
    }


    function createCellWithLink(url, text) {
        const td = document.createElement('td');
        const a = document.createElement('a');
        a.href = url;
        a.innerText = text;
        a.target = '_blank';
        a.style.color = '#007bff';
        a.style.textDecoration = 'none';
        a.addEventListener('mouseover', () => { a.style.textDecoration = 'underline'; });
        a.addEventListener('mouseout', () => { a.style.textDecoration = 'none'; });
        td.appendChild(a);
        return td;
    }

    function createCell(content) {
        const td = document.createElement('td');
        td.innerText = content;
        return td;
    }

    function createCellWithImage(src, alt) {
        const td = document.createElement('td');
        const img = document.createElement('img');
        img.src = src;
        img.alt = alt;
        img.style.height = '30px';
        img.setAttribute('loading', 'lazy');
        td.appendChild(img);
        return td;
    }

    function formatTimestamp(unixTime) {
        if (unixTime.toString().length === 10) {
            unixTime = unixTime * 1000;
        }
        const date = new Date(unixTime);
        const now = new Date();
        const diff = Math.floor((now - date) / 1000);

        if (diff < 60) {
            return diff + 's ago';
        }
        const minutes = Math.floor(diff / 60);
        if (minutes < 60) {
            return minutes + 'm ago';
        }
        const hours = Math.floor(minutes / 60);
        if (hours < 24) {
            return hours + 'h ago';
        }
        const days = Math.floor(hours / 24);
        return days + 'd ago';
    }

    function ensureBazaarEnhancerContainer() {
        return new Promise(resolve => {
            if (document.querySelector('.captcha-container')) {
                return;
            }
            let container = document.getElementById('bazaar-enhancer-container');
            if (container) {
                return resolve(container);
            }
            container = document.createElement('div');
            container.id = 'bazaar-enhancer-container';
            container.style.overflow = 'hidden';

            let target = document.querySelector('.delimiter___zFh2E');
            if (target && target.parentNode) {
                target.parentNode.insertBefore(container, target.nextSibling);
                return resolve(container);
            }
            const observer = new MutationObserver((mutations, obs) => {
                if (document.querySelector('.captcha-container')) {
                    obs.disconnect();
                    return;
                }
                target = document.querySelector('.delimiter___zFh2E');
                if (target && target.parentNode) {
                    target.parentNode.insertBefore(container, target.nextSibling);
                    obs.disconnect();
                    return resolve(container);
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });

        });
    }

    function initTabbedInterface(container) {
        container.innerHTML = '';

        const nav = document.createElement('div');
        nav.id = 'bazaar-nav';
        nav.style.display = 'flex';
        nav.style.justifyContent = 'center';
        nav.style.marginBottom = '10px';

        const btnTopCheapest = document.createElement('button');
        btnTopCheapest.innerText = 'Top 3';
        btnTopCheapest.addEventListener('click', () => { setActiveTab(0); });

        const btnFullListings = document.createElement('button');
        btnFullListings.innerText = 'All Bazaars';
        btnFullListings.addEventListener('click', () => { setActiveTab(1); });

        nav.appendChild(btnTopCheapest);
        nav.appendChild(btnFullListings);
        container.appendChild(nav);

        const topCheapestView = document.createElement('div');
        topCheapestView.id = 'topCheapestView';
        topCheapestView.style.padding = '10px';
        topCheapestView.innerHTML = '<p>Loading top cheapest listings...</p>';

        const fullListingsView = document.createElement('div');
        fullListingsView.id = 'fullListingsView';
        fullListingsView.style.padding = '10px';
        fullListingsView.style.maxHeight = '350px';
        fullListingsView.style.height = 'auto';
        fullListingsView.style.overflowY = 'auto';
        fullListingsView.innerHTML = '<p>Loading full listings...</p>';

        container.appendChild(topCheapestView);
        container.appendChild(fullListingsView);

        setActiveTab(0);
    }

    function setActiveTab(tabIndex) {
        const topCheapestView = document.getElementById('topCheapestView');
        const fullListingsView = document.getElementById('fullListingsView');
        if (tabIndex === 0) {
            topCheapestView.style.display = 'block';
            fullListingsView.style.display = 'none';
        } else {
            topCheapestView.style.display = 'none';
            fullListingsView.style.display = 'block';
        }
    }

    function sortItems(items) {
        return items.slice().sort((a, b) => {
            let valA = a[sortKey], valB = b[sortKey];
            if (valA < valB) return sortOrder === 'asc' ? -1 : 1;
            if (valA > valB) return sortOrder === 'asc' ? 1 : -1;
            return 0;
        });
    }

    function renderListings() {
        if (currentItemData && allBazaarItems.length > 0) {
            const sortedItems = sortItems(allBazaarItems);
            const fullListingsView = document.getElementById('fullListingsView');
            const topCheapestView = document.getElementById('topCheapestView');
            displayFullListings(sortedItems, fullListingsView);
            displayTopCheapestItems(sortedItems.slice(0, 3), currentItemData.itemName, topCheapestView);
        }
    }

    function createSortableHeader(text, key) {
        const th = document.createElement('th');
        th.innerText = text;
        th.style.border = '1px solid #ccc';
        th.style.padding = '8px';
        th.style.backgroundColor = '#e0e0e0';
        th.style.textAlign = 'center';
        th.style.fontSize = '14px';
        th.style.cursor = 'pointer';
        th.addEventListener('click', () => {
            if (sortKey === key) {
                sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
            } else {
                sortKey = key;
                sortOrder = 'asc';
            }
            renderListings();
        });
        return th;
    }

    function fetchBazaarItems(itemID) {
        if (!itemID) {
            return;
        }
        const fullListingsView = document.getElementById('fullListingsView');
        const topCheapestView = document.getElementById('topCheapestView');
        if (ongoingRequests.has(`bazaar_items_${itemID}`)) return;
        ongoingRequests.add(`bazaar_items_${itemID}`);
        fullListingsView.innerHTML = `<p>Loading full listings...</p><div class="loading-spinner"></div>`;
        topCheapestView.innerHTML = `<p>Loading top 3 cheapest items...</p><div class="loading-spinner"></div>`;
        GM_xmlhttpRequest({
            method: 'GET',
            url: `${BACKEND_URL}/get_bazaar_items/${itemID}`,
            headers: { 'Accept': 'application/json' },
            onload: function(response) {
                ongoingRequests.delete(`bazaar_items_${itemID}`);
                if (response.status === 200) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.bazaar_items) {
                            allBazaarItems = data.bazaar_items;
                            const sortedItems = sortItems(allBazaarItems);
                            displayFullListings(sortedItems, fullListingsView);
                            displayTopCheapestItems(sortedItems.slice(0, 3), currentItemData ? currentItemData.itemName : "", topCheapestView);
                        } else {
                            fullListingsView.innerHTML = `<p>No items found.</p>`;
                            topCheapestView.innerHTML = `<p>No items found.</p>`;
                        }
                    } catch(e) {
                        fullListingsView.innerHTML = `<p>Error parsing server response.</p>`;
                        topCheapestView.innerHTML = `<p>Error parsing server response.</p>`;
                        console.error("Error parsing bazaar items response:", e);
                    }
                } else {
                    fullListingsView.innerHTML = `<p>Error: ${response.status} - ${response.statusText}</p>`;
                    topCheapestView.innerHTML = `<p>Error: ${response.status} - ${response.statusText}</p>`;
                }
            },
            onerror: function(error) {
                ongoingRequests.delete(`bazaar_items_${itemID}`);
                fullListingsView.innerHTML = `<p>Network error occurred. Please try again later.</p>`;
                topCheapestView.innerHTML = `<p>Network error occurred. Please try again later.</p>`;
                console.error("Network error (bazaar items):", error);
            }
        });
    }

    function displayFullListings(items, targetElement) {
        targetElement.innerHTML = '';
        if (items.length === 0) {
            targetElement.innerHTML = `<p>No items found.</p>`;
            return;
        }
        const title = document.createElement('h3');
        title.innerText = `Full Listings`;
        title.style.textAlign = 'center';
        title.style.marginTop = '2px';
        title.style.marginBottom = '10px';
        targetElement.appendChild(title);

        const tableContainer = document.createElement('div');
        tableContainer.style.overflowX = 'auto';
        tableContainer.style.width = '100%';

        const table = document.createElement('table');
        table.className = 'top-cheapest-table';
        table.style.width = '100%';
        table.style.borderCollapse = 'collapse';

        const thead = document.createElement('thead');
        const headerRow = document.createElement('tr');
        headerRow.appendChild(createSortableHeader("Price ($) ↑↓", "price"));
        headerRow.appendChild(createSortableHeader("Quantity ↑↓", "quantity"));
        headerRow.appendChild(createSortableHeader("Updated ↑↓", "last_updated"));
        headerRow.appendChild(createCell("Seller"));
        thead.appendChild(headerRow);
        table.appendChild(thead);

        const tbody = document.createElement('tbody');
        items.forEach((item, index) => {
            const tr = document.createElement('tr');
            tr.appendChild(createCellWithLink(
                `https://www.torn.com/bazaar.php?userID=${item.user_id}`,
                `$${item.price.toLocaleString()}`
            ));
            tr.appendChild(createCell(item.quantity));
            tr.appendChild(createCell(formatTimestamp(item.last_updated)));
            const sellerText = item.player_name ? item.player_name : item.user_id;
            tr.appendChild(createCellWithLink(
                `https://www.torn.com/profiles.php?XID=${item.user_id}`,
                sellerText
            ));
            tbody.appendChild(tr);
        });
        table.appendChild(tbody);
        tableContainer.appendChild(table);
        targetElement.appendChild(tableContainer);
        adjustUnifiedTableTheme();
    }

    function displayTopCheapestItems(items, itemName, targetElement) {
        targetElement.innerHTML = '';
        if (!items || items.length === 0) {
            targetElement.innerHTML = `<p>No items found.</p>`;
            return;
        }
        const title = document.createElement('h3');
        title.innerText = `Top 3 Cheapest ${itemName} Bazaar Items`;
        title.style.textAlign = 'center';
        title.style.marginTop = '2px';
        title.style.marginBottom = '10px';
        targetElement.appendChild(title);

        const tableContainer = document.createElement('div');
        tableContainer.style.overflowX = 'auto';
        tableContainer.style.width = '100%';

        const table = document.createElement('table');
        table.className = 'top-cheapest-table';
        table.style.width = '100%';
        table.style.borderCollapse = 'collapse';

        const thead = document.createElement('thead');
        const headerRow = document.createElement('tr');
        headerRow.appendChild(createSortableHeader("Price ($) ↑↓", "price"));
        headerRow.appendChild(createSortableHeader("Quantity ↑↓", "quantity"));
        headerRow.appendChild(createSortableHeader("Updated ↑↓", "last_updated"));
        headerRow.appendChild(createCell("Seller"));
        thead.appendChild(headerRow);
        table.appendChild(thead);

        const tbody = document.createElement('tbody');
        items.slice(0, 3).forEach((item, index) => {
            const tr = document.createElement('tr');
            tr.appendChild(createCellWithLink(
                `https://www.torn.com/bazaar.php?userID=${item.user_id}`,
                `$${item.price.toLocaleString()}`
            ));
            const quantityTd = document.createElement('td');
            quantityTd.innerText = item.quantity;
            quantityTd.style.border = '1px solid #ccc';
            quantityTd.style.padding = '6px';
            quantityTd.style.textAlign = 'center';
            quantityTd.style.fontSize = '14px';
            tr.appendChild(quantityTd);
            const updatedTd = document.createElement('td');
            updatedTd.innerText = formatTimestamp(item.last_updated);
            updatedTd.style.border = '1px solid #ccc';
            updatedTd.style.padding = '6px';
            updatedTd.style.textAlign = 'center';
            updatedTd.style.fontSize = '14px';
            tr.appendChild(updatedTd);
            const sellerText = item.player_name ? item.player_name : item.user_id;
            tr.appendChild(createCellWithLink(
                `https://www.torn.com/profiles.php?XID=${item.user_id}`,
                sellerText
            ));
            tbody.appendChild(tr);
        });
        table.appendChild(tbody);
        tableContainer.appendChild(table);
        targetElement.appendChild(tableContainer);
        adjustUnifiedTableTheme();
    }


    function adjustUnifiedTableTheme() {
        const isDarkMode = document.body.classList.contains('dark-mode');
        const tables = document.querySelectorAll('.top-cheapest-table');
        tables.forEach(table => {
            if (isDarkMode) {
                table.style.backgroundColor = '#1c1c1c';
                table.style.color = '#f0f0f0';
                table.querySelectorAll('th').forEach(th => {
                    th.style.backgroundColor = '#444';
                    th.style.color = '#ffffff';
                });
                table.querySelectorAll('tr:nth-child(even)').forEach(tr => {
                    tr.style.backgroundColor = '#2a2a2a';
                });
                table.querySelectorAll('tr:nth-child(odd)').forEach(tr => {
                    tr.style.backgroundColor = '#1e1e1e';
                });
                table.querySelectorAll('td a').forEach(a => {
                    a.style.color = '#4ea8de';
                });
            } else {
                table.style.backgroundColor = '#fff';
                table.style.color = '#000';
                table.querySelectorAll('th').forEach(th => {
                    th.style.backgroundColor = '#f2f2f2';
                    th.style.color = '#000';
                });
                table.querySelectorAll('tr:nth-child(even)').forEach(tr => {
                    tr.style.backgroundColor = '#f9f9f9';
                });
                table.querySelectorAll('tr:nth-child(odd)').forEach(tr => {
                    tr.style.backgroundColor = '#fff';
                });
                table.querySelectorAll('td a').forEach(a => {
                    a.style.color = '#007bff';
                });
            }
        });
    }

    function adjustBazaarEnhancerContainerTheme() {
        const container = document.getElementById('bazaar-enhancer-container');
        const isDarkMode = document.body.classList.contains('dark-mode');
        if (container) {
            if (isDarkMode) {
                container.style.backgroundColor = 'rgba(0,0,0,0.6)';
                container.style.color = '#f0f0f0';
                container.style.border = '1px solid rgba(255,255,255,0.1)';
                container.style.boxShadow = '0 4px 8px rgba(0,0,0,0.4)';
            } else {
                container.style.backgroundColor = '#ffffff';
                container.style.color = '#000000';
                container.style.border = '1px solid #ddd';
                container.style.boxShadow = '0 4px 8px rgba(0,0,0,0.1)';
            }
        }
    }

    function observeDarkMode() {
        const observer = new MutationObserver(() => {
            adjustUnifiedTableTheme();
            adjustBazaarEnhancerContainerTheme();
        });
        observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
    }

    function injectAdditionalStyles() {
        const style = document.createElement('style');
        style.type = 'text/css';
        style.innerHTML = `
    @keyframes spin {
        0% { transform: rotate(0deg); }
        100% { transform: rotate(360deg); }
    }
    #bazaar-enhancer-container {
        background-color: #ffffff;
        color: #000000;
        border: 1px solid #ddd;
        box-shadow: 0 4px 8px rgba(0,0,0,0.1);
        border-radius: 8px;
        padding: 10px;
        margin: 10px 0;
        transition: background-color 0.3s, color 0.3s;
    }
    .dark-mode #bazaar-enhancer-container {
        background-color: rgba(0,0,0,0.6);
        color: #f0f0f0;
        border: 1px solid rgba(255,255,255,0.1);
        box-shadow: 0 4px 8px rgba(0,0,0,0.4);
    }
    #showBazaarModal table.bazaar-table,
    #bazaar-enhancer-container table.top-cheapest-table {
        width: 100%;
        border-collapse: collapse;
        margin-top: 10px;
        table-layout: auto;
    }
    #showBazaarModal table.bazaar-table th,
    #showBazaarModal table.bazaar-table td,
    #bazaar-enhancer-container table.top-cheapest-table th,
    #bazaar-enhancer-container table.top-cheapest-table td {
        text-align: center;
        padding: 8px;
        border: 1px solid #ccc;
    }
    #showBazaarModal table.bazaar-table th,
    #bazaar-enhancer-container table.top-cheapest-table th {
        background-color: #f2f2f2;
    }
    .dark-mode #showBazaarModal table.bazaar-table th,
    .dark-mode #showBazaarModal table.bazaar-table td,
    .dark-mode #bazaar-enhancer-container table.top-cheapest-table th,
    .dark-mode #bazaar-enhancer-container table.top-cheapest-table td {
        background-color: #2a2a2a;
        color: #f0f0f0;
    }
    .dark-mode #showBazaarModal table.bazaar-table th,
    .dark-mode #bazaar-enhancer-container table.top-cheapest-table th {
        background-color: #333;
    }
    #showBazaarModal .loading-spinner,
    #bazaar-enhancer-container .loading-spinner {
        border: 4px solid #f3f3f3;
        border-top: 4px solid #3498db;
        border-radius: 50%;
        width: 24px;
        height: 24px;
        animation: spin 2s linear infinite;
        display: inline-block;
        margin-left: 10px;
    }
    #bazaar-enhancer-container a.visited-link,
    #bazaar-enhancer-container table a.visited-link,
    #showBazaarModal a.visited-link,
    #showBazaarModal table a.visited-link {
        color: purple !important;
    }
    #bazaar-nav {
        display: flex;
        justify-content: center;
        margin-bottom: 10px;
    }
    #bazaar-nav button {
        margin: 0 5px;
        padding: 5px 10px;
        cursor: pointer;
        background-color: #f2f2f2;
        color: #000;
        border: 1px solid #ccc;
    }
    .dark-mode #bazaar-nav button {
        background-color: #444;
        color: #fff;
        border: 1px solid #666;
    }
    #topCheapestView, #fullListingsView {
        width: 100%;
        box-sizing: border-box;
        padding: 10px;
    }
    #fullListingsView {
        height: 500px;
        overflow-y: auto;
    }
    `;
        document.head.appendChild(style);
    }


    function checkForItems(wrapper) {
        if (!wrapper || wrapper.id === 'bazaar-enhancer-container') return;
        let itemTile = wrapper.previousElementSibling;
        if (itemTile && itemTile.id === 'bazaar-enhancer-container') {
            itemTile = itemTile.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];
            currentItemData = {
                itemID: parseInt(itemId, 10),
                itemName: itemName
            };
            fetchBazaarItems(currentItemData.itemID);
        }
    }

    function checkForItemsMobile() {
        if (window.innerWidth >= 784) return;
        const sellerList = document.querySelector('ul.sellerList___e4C9_');

        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 = null;
        if (btn) {
            const parts = btn.getAttribute('aria-controls').split('-');
            itemId = parts.length > 2 ? parts[parts.length - 2] : parts[parts.length - 1];
        }
        if (!itemId) return;
        currentItemData = {
            itemID: parseInt(itemId, 10),
            itemName: itemName
        };
        fetchBazaarItems(currentItemData.itemID);
    }

    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType !== Node.ELEMENT_NODE) return;
                if (window.innerWidth < 784 && node.classList.contains('sellerList___e4C9_')) {
                    checkForItemsMobile();
                } else if (window.innerWidth >= 784 && node.className.includes("sellerListWrapper")) {
                    checkForItems(node);
                }
            });
        });
    });
    observer.observe(document.body, { childList: true, subtree: true });

    setInterval(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            setTimeout(() => {
                let info = getItemInfoFromURL();
                if (info.itemID) {
                    currentItemData = info;
                    fetchBazaarItems(info.itemID);
                } else {
                    if (window.innerWidth < 784) {
                        checkForItemsMobile();
                    } else {
                        const wrapper = document.querySelector('[class*="sellerListWrapper"]');
                        if (wrapper) checkForItems(wrapper);
                        else {
                            clearListingsData();
                            currentItemData = null;
                        }
                    }
                }
            }, 100);
        }
    }, 500);
    init()
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址