View Listings Anytime

View your listings on the Points Market, Bazaar and Item Market even when the page is unavailable!

// ==UserScript==
// @name         View Listings Anytime
// @namespace    heartflower.torn
// @version      1.0.1
// @description  View your listings on the Points Market, Bazaar and Item Market even when the page is unavailable!
// @author       Heartflower [2626587]
// @match        https://www.torn.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // Change these variables if you don't want to use parts of the script
    let bazaar = true;
    let itemmarket = true;
    let pointsmarket = true;
    let listings = true;

    console.log('[HF] View Listings Running');

    let apiKey;
    let storedAPIKey = localStorage.getItem('hf-full-access-apiKey');

    if (storedAPIKey) {
        apiKey = storedAPIKey;
        if (typeof GM_registerMenuCommand === 'function') GM_registerMenuCommand('Remove API key', removeAPIKey);
    } else {
        setAPIkey();
    }

    // VARIABLES TO USE LATER //
    let maximumCalls = 40;
    let apiCallCount = 0;
    let itemIDs = [];
    let itemUIDs = [];
    let userName = 'Unknown';
    let userID = '';
    let personalListings = [];
    let timestamp = 0;
    let hospitalTimestamp = 0;


    // API SETTINGS //

    function setAPIkey() {
        let enterAPIKey = prompt('Enter a full access API key here:');

        if (enterAPIKey !== null && enterAPIKey.trim() !== '') {
            localStorage.setItem('hf-full-access-apiKey', enterAPIKey);
            alert('API key set succesfully');

            apiKey = enterAPIKey;

            if (typeof GM_registerMenuCommand === 'function') GM_registerMenuCommand('Remove API key', removeAPIKey);
        } else {
            alert('No valid API key entered!');

            if (typeof GM_registerMenuCommand === 'function') GM_registerMenuCommand('Set API key', setAPIkey);
        }
    }

    function removeAPIKey() {
        let wantToDelete = confirm('Are you sure you want to remove your API key?');

        if (wantToDelete) {
            localStorage.removeItem('hf-full-access-apiKey');
            alert('API key successfully removed.');
        } else {
            alert('API key not removed.');
        }
    }

    function createAPIlink(type, retries = 30) {
        let existingLink = document.body.querySelector('.hf-api-link');
        if (existingLink) existingLink.remove();

        let titleContainer = document.body.querySelector('.content-title');
        if (!titleContainer) titleContainer = document.body.querySelector('.info-msg');
        if (!titleContainer) {
            if (retries > 0) {
                setTimeout(() => createAPIlink(type, retries - 1), 100);
            } else {
                console.warn('[HF] Gave up looking for title container after 30 retries.');
            }
            return;
        }

        let msg = document.body.querySelector('.info-msg .msg');

        // Create the link to remove / add API key
        let div = document.createElement('div');
        if (msg) div = document.createElement('span');
        div.className = 'hf-api-link';
        div.style.marginLeft = '4px';
        div.style.marginBottom = '10px';
        div.style.color = 'var(--default-blue-color)';
        div.style.cursor = 'pointer';

        if (type === 'remove') {
            div.textContent = 'Remove your full access API key';

            div.addEventListener('click', function() {
                removeAPIKey();
            });
        } else if (type === 'add') {
            div.textContent = 'Enter your full access API key';

            div.addEventListener('click', function() {
                setAPIkey();
            });
        }

        if (msg) {
            msg.appendChild(div)
        } else {
            titleContainer.parentNode.insertBefore(div, titleContainer.nextSibling);
        }
    }



    // SIDEBAR //

    function createContainer(type, retries = 30) {
        let mobile = !document.body.querySelector('.searchFormWrapper___LXcWp');
        if (mobile) {
            let headerMenu = document.body.querySelector('.leftMenu___md3Ch');
            createObserver(headerMenu);
            return;
        }

        let existingButton = document.body.querySelector(`.hf-view-listings-container-${type}`);
        if (existingButton) return;

        let toggleBlocks = document.body.querySelectorAll('.toggle-block___oKpdF');
        if (!toggleBlocks || toggleBlocks.length < 3) {
            if (retries > 0) {
                setTimeout(() => createContainer(type, retries - 1), 100);
            } else {
                console.warn('[HF] Gave up looking for sidebar after 30 retries.');
            }
            return;
        }

        let container = toggleBlocks[2].querySelector('.toggle-content___BJ9Q9');
        let classes = container.querySelector('div').classList;

        let listingsContainer = document.createElement('div');
        listingsContainer.classList.add(...classes);
        listingsContainer.classList.add(`hf-view-listings-container-${type}`);
        container.appendChild(listingsContainer);

        let titleDiv = document.createElement('div');
        titleDiv.textContent = `My ${type} Listings`;
        titleDiv.classList.add('area-row___iBD8N');
        titleDiv.style.padding = '5px 10px';
        titleDiv.style.fontWeight = 'bold';
        titleDiv.style.display = 'flex';
        titleDiv.style.justifyContent = 'space-between';
        listingsContainer.appendChild(titleDiv);

        if (type === 'Bazaar') {
            titleDiv.style.background = 'var(--title-msg-blue-gradient)';
        } else if (type === 'Market') {
            titleDiv.style.background = 'var(--title-msg-green-gradient)';
        }

        let arrow = document.createElement('span');
        arrow.textContent = '►';
        titleDiv.appendChild(arrow);

        titleDiv.addEventListener('click', function() {
            let arrowText = arrow.textContent;
            if (arrowText === '►') {
                arrow.textContent = '▼';

                showListings(type, listingsContainer);
            } else {
                arrow.textContent = '►';

                let scrollArea = listingsContainer.querySelector('.hf-view-listing-scrollarea');
                scrollArea.remove();
            }
        });
    }

    async function showListings(type, listingsContainer) {
        let scrollArea = document.createElement('div');
        scrollArea.classList.add('scrollarea', 'scroll-area___zOH66', 'hf-view-listing-scrollarea')
        listingsContainer.appendChild(scrollArea);

        let content = document.createElement('div');
        content.classList.add('scrollarea-content');
        scrollArea.appendChild(content);

        let ul = document.createElement('ul');
        ul.classList.add('list___NuD9d');
        ul.style.listStyle = 'disc';
        content.appendChild(ul);

        if (type === 'Bazaar') {
            fetchBazaar(ul);
        } else if (type === 'Market') {
            fetchMarket(ul);
        }

        let scrollbarContainer = document.createElement('div');
        scrollbarContainer.classList.add('scrollbar-container', 'vertical');
        scrollArea.appendChild(scrollbarContainer);

        let scrollbar = document.createElement('div');
        scrollbar.classList.add('scrollbar');
        scrollbarContainer.appendChild(scrollbar);

        // Scroll container setup
        let scrollTop = 0;

        let scrollAreaHeight = scrollArea.clientHeight;
        let contentHeight = content.scrollHeight;
        let scrollbarHeight = (scrollAreaHeight / contentHeight) * scrollAreaHeight;

        // Minimum height for usability
        scrollbarHeight = Math.max(scrollbarHeight, 140);
        scrollbar.style.height = `${scrollbarHeight}px`;

        function updateScroll() {
            const maxScrollContent = Math.max(0, content.scrollHeight - scrollArea.clientHeight);
            const maxScrollBar = Math.max(0, scrollArea.clientHeight - scrollbar.clientHeight);

            // Clamp scrollTop within bounds
            scrollTop = Math.min(Math.max(0, scrollTop), maxScrollContent);

            // Move content and scrollbar
            content.style.transform = `translateY(-${scrollTop}px)`;
            let scrollbarOffset = scrollTop / maxScrollContent * maxScrollBar || 0;
            scrollbar.style.transform = `translateY(${scrollbarOffset}px)`;
        }

        // Mouse wheel scrolling
        scrollArea.addEventListener('wheel', (e) => {
            e.preventDefault();

            // Optional: clamp delta speed
            let delta = Math.max(-60, Math.min(60, e.deltaY));

            const maxScrollContent = Math.max(0, content.scrollHeight - scrollArea.clientHeight);
            scrollTop = Math.min(Math.max(0, scrollTop + delta), maxScrollContent);

            updateScroll();
        });

        // Drag to scroll
        let isDragging = false;
        let dragStartY = 0;
        let initialScrollTop = 0;

        scrollbar.addEventListener('mousedown', (e) => {
            e.preventDefault();
            isDragging = true;
            dragStartY = e.clientY;
            initialScrollTop = scrollTop;
            document.body.style.userSelect = 'none'; // Prevent text selection
        });

        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;

            const deltaY = e.clientY - dragStartY;
            const maxScrollContent = Math.max(0, content.scrollHeight - scrollArea.clientHeight);
            const maxScrollBar = Math.max(0, scrollArea.clientHeight - scrollbar.clientHeight);

            const scrollRatio = maxScrollContent / maxScrollBar;
            scrollTop = Math.min(Math.max(0, initialScrollTop + deltaY * scrollRatio), maxScrollContent);

            updateScroll();
        });

        document.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                document.body.style.userSelect = '';
            }
        });

        // Touch to scroll (mobile support)
        let touchStartY = 0;
        let touchInitialScrollTop = 0;

        scrollArea.addEventListener('touchstart', (e) => {
            if (e.touches.length !== 1) return;
            touchStartY = e.touches[0].clientY;
            touchInitialScrollTop = scrollTop;
        }, { passive: false });

        scrollArea.addEventListener('touchmove', (e) => {
            if (!scrollArea.contains(e.target)) return;

            if (e.touches.length !== 1) return;
            e.preventDefault(); // prevent native scrolling

            const deltaY = touchStartY - e.touches[0].clientY;
            const maxScrollContent = Math.max(0, content.scrollHeight - scrollArea.clientHeight);
            scrollTop = Math.min(Math.max(0, touchInitialScrollTop + deltaY), maxScrollContent);

            updateScroll();
        }, { passive: false });

    }

    async function createList(type, ul, data, refreshed, mobile) {
        if (refreshed) {
            let lists = ul.querySelectorAll('li');
            for (let list of lists) {
                list.remove();
            }
        }

        for (let item of data) {
            let name = 'Unknown';
            let qty = 0;
            let price = 0;

            if (type === 'Bazaar') {
                name = item.name;
                // id = item.id;
                // img_src = `images/items/${id}/medium.png`;
                qty = item.quantity;
                price = item.price;
            } else if (type === 'Market') {
                name = item.item.name;
                qty = item.amount;
                price = item.price;
            }

            let totalPrice = price * qty;

            let li = document.createElement('li');
            li.style.margin = '0px 22px';
            li.style.display = 'list-item';
            li.style.padding = '0px';
            li.style.cursor = 'auto';
            li.style.height = 'auto';
            li.style.lineHeight = 'normal';
            li.style.marginBottom = '4px';
            li.textContent = `${name} (${qty})`;
            li.title = `$${totalPrice.toLocaleString('en-US')}`;
            ul.appendChild(li);
        }

        if (refreshed) {
            let span = ul.querySelector('.hf-refresh-span');
            ul.appendChild(span);
            return;
        }

        let refresh = document.createElement('span');
        refresh.classList.add('hf-refresh-span');
        refresh.textContent = 'Refresh my listings';
        refresh.style.marginTop = '4px';
        refresh.style.marginLeft = '8px';
        refresh.style.color = 'var(--default-blue-color)';
        refresh.style.cursor = 'pointer';
        refresh.style.paddingBottom = '8px';
        ul.parentNode.appendChild(refresh);

        refresh.addEventListener('click', function() {
            if (type === 'Bazaar') {
                fetchBazaar(ul, true);
            } else if (type === 'Market') {
                fetchMarket(ul, true);
            }
        });
    }

    function createMobileMenu(ul) {
        let existingMenu = document.body.querySelector('.hf-view-listings-menu');
        if (existingMenu) return;

        let li = document.createElement('li');
        li.classList.add('menu-item-link');
        li.classList.add('hf-view-listings-menu');
        li.style.paddingLeft = '12px';
        li.style.color = 'var(--default-color)';
        li.textContent = 'View Listings';
        ul.appendChild(li);

        li.addEventListener('click', function() {
            window.open('https://www.torn.com/hf-viewlistings', '_self');
        });
    }

    function changeMobilePage(retries = 30) {
        let title = document.body.querySelector('#skip-to-content');
        if (!title) {
            if (retries > 0) {
                setTimeout(() => changeMobilePage(retries - 1), 100);
            } else {
                console.warn('[HF] Gave up looking for title after 30 retries.');
            }
            return;
        }

        title.textContent = 'View Listings';

        let contentWrapper = document.body.querySelector('.content-wrapper');

        let errorWrap = contentWrapper.querySelector('.error-404');
        errorWrap.remove();

        let div = document.createElement('div');

        contentWrapper.appendChild(div);

        showMobileListings('Market', div);
        showMobileListings('Bazaar', div)
    }

    function showMobileListings(type, div) {
        let existingContainer = document.body.querySelector(`hf-listings-container-${type}`);
        if (existingContainer) return;

        let container = document.createElement('div');
        container.classList.add(`hf-listings-container-${type}`);

        let titleDiv = document.createElement('div');
        titleDiv.style.background = 'var(--title-msg-blue-gradient)';
        titleDiv.style.padding = '6px';
        titleDiv.style.borderRadius = '5px';
        titleDiv.style.fontWeight = 'bold';
        titleDiv.style.display = 'flex';
        titleDiv.style.justifyContent = 'space-between';
        titleDiv.style.alignItems = 'center';
        container.appendChild(titleDiv);

        let title = document.createElement('span');
        titleDiv.appendChild(title);

        let refresh = document.createElement('span');
        refresh.classList.add('link-icon-svg', 'refresh');
        refresh.innerHTML = `<svg style="width: 15px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 14.47"><defs><style>.cls-1{opacity:0.35;}.cls-2{fill:var(--default-color);}.cls-3{fill:var(--default-color);}</style></defs><g id="Слой_2" data-name="Слой 2"><g id="icons"><g class="cls-1"><path class="cls-2" d="M1.68,7.74A5.05,5.05,0,0,1,11.5,6.05H8.42l3.84,4L16,6.05H13.26A6.74,6.74,0,1,0,11,13l-1.06-1.3A5.06,5.06,0,0,1,1.68,7.74"></path></g><path class="cls-3" d="M1.68,6.74A5.05,5.05,0,0,1,11.5,5.05H8.42l3.84,4L16,5.05H13.26A6.74,6.74,0,1,0,11,12l-1.06-1.3A5.06,5.06,0,0,1,1.68,6.74"></path></g></g></svg>`
        refresh.style.cursor = 'pointer';
        titleDiv.appendChild(refresh);

        refresh.addEventListener('click', function() {
            if (type === 'Bazaar') {
                fetchBazaar(ulContainer, true, true);
            } else if (type === 'Market') {
                fetchMarket(ulContainer, true, true);
            }
        });

        if (type === 'Market') {
            title.textContent = 'Item Market';
        } else if (type === 'Bazaar') {
            title.textContent = 'Bazaar';
            container.style.paddingTop = '20px';
        }

        let ulContainer = document.createElement('div');
        ulContainer.style.display = 'flex';
        ulContainer.style.justifyContent = 'space-between';

        if (type === 'Bazaar') {
            fetchBazaar(ulContainer, null, true);
        } else if (type === 'Market') {
            fetchMarket(ulContainer, null, true);
        }

        container.appendChild(ulContainer);
        div.appendChild(container);
    }

    function createMobileList(type, container, data, refreshed) {
        if (refreshed) {
            let uls = container.querySelectorAll('ul');
            for (let ul of uls) {
                ul.remove();
            }
        }

        let leftUl = document.createElement('ul');
        leftUl.style.listStyle = ('disc');
        leftUl.style.padding = '8px 0px 0px 20px';
        leftUl.style.flex = '1';
        container.appendChild(leftUl);

        let rightUl = document.createElement('ul');
        rightUl.style.listStyle = ('disc');
        rightUl.style.padding = '8px 0px 0px 20px';
        rightUl.style.flex = '1';
        container.appendChild(rightUl);

        data.forEach((item, index) => {
            let name = type === 'Bazaar' ? item.name : item.item.name;
            let qty = type === 'Bazaar' ? item.quantity : item.amount;
            let price = item.price;
            let totalPrice = qty * price;

            let li = document.createElement('li');
            li.textContent = `${name} (${qty})`;
            li.title = `$${totalPrice.toLocaleString('en-US')}`;
            li.style.marginBottom = '4px';

            (index % 2 === 0 ? leftUl : rightUl).appendChild(li);
        });
    }



    // POINTS MARKET //

    function pointsMarketPage(retries = 30) {
        let contentWrapper = document.body.querySelector('.content-wrapper');
        if (!contentWrapper) {
            if (retries > 0) {
                setTimeout(() => pointsMarketPage(retries - 1), 100);
            } else {
                console.warn('[HF] Gave up looking for content wrapper after 30 retries.');
            }
            return;
        }

        let brokenWindow = document.body.querySelector('.error-404');
        if (brokenWindow) brokenWindow.remove();

        let messageContainer = document.body.querySelector('.info-msg');
        let message = messageContainer.querySelector('.msg');
        let messageContent = 'This area is unavailable';
        if (message) messageContent = message.textContent;
        if (!messageContent.includes('This area is unavailable')) return;

        let originalTitle = document.querySelector('h4#skip-to-content');
        if (originalTitle) originalTitle.parentNode.remove();

        // Create the points market title as it's not usually there
        let titleContainer = document.createElement('div');

        let title = document.createElement('p');
        title.id = 'hf-pmv-title';
        title.textContent = 'Points Market';

        // Add a line break after the title
        let hrElement = document.createElement('hr');
        hrElement.className = 'page-head-delimiter m-top10 m-bottom10';

        titleContainer.appendChild(title);
        titleContainer.appendChild(hrElement);
        contentWrapper.insertBefore(titleContainer, contentWrapper.firstChild);

        // Change the message
        if (message) message.innerHTML = `The points market is fetched by the <p style='font-style:italic; display:inline'>View Listings Anytime</p> script with the help of the API!`;
        if (messageContainer) messageContainer.style.background = 'rgb(85,137,33)';

        fetchLogs();
        fetchHospitalTime();
        createAPIlink('remove');
    }


    // Fetch and display the time remaining in the hospital
    function changeTimer(data, messageContainer, message) {
        let status = data.status;
        hospitalTimestamp = status.until;

        let timeRemaining = 0;

        let timerElement = document.createElement('div');
        timerElement.id = 'hf-pmv-hospital-timer';
        timerElement.style.display = 'inline';
        timerElement.style.paddingLeft = '4px';

        message.insertBefore(timerElement, message.lastChild);

        updateTimer();
        setInterval(updateTimer, 100);

    }

    // Function to calculate remaining time
    function calculateTimeRemaining() {
        let currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
        let timeDifference = hospitalTimestamp - currentTime;
        if (timeDifference <= 0) return "You're out of the hospital!";

        let days = Math.floor(timeDifference / (24 * 60 * 60));
        let hours = Math.floor((timeDifference % (24 * 60 * 60)) / (60 * 60));
        let minutes = Math.floor((timeDifference % (60 * 60)) / 60);
        let seconds = timeDifference % 60;

        let remainingTime = "You will be out of the hospital in ";
        if (days > 0) remainingTime += `${days} ${days === 1 ? 'day' : 'days'}, `;
        if (hours > 0) remainingTime += `${hours} ${hours === 1 ? 'hour' : 'hours'}, `;
        if (minutes > 0) remainingTime += `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} and `;
        remainingTime += `${seconds} ${seconds === 1 ? 'second' : 'seconds'}`;
        remainingTime += '.';

        return remainingTime;
    }

    // Function to update timer display
    function updateTimer(hospitalTimestamp) {
        let timerElement = document.getElementById('hf-pmv-hospital-timer');
        if (timerElement) {
            timerElement.textContent = calculateTimeRemaining(hospitalTimestamp);
        }
    }

    // Create the table
    function createPointsTable(data) {
        let contentWrapper = document.body.querySelector('.content-wrapper');
        if (!contentWrapper) return;

        let existingContainer = document.getElementById('hf-pmv-table');
        if (existingContainer) return;

        let tableRows = [];

        // Sort listing IDs by cost
        for (let listingID in data) {
            let listing = data[listingID];
            let cost = listing.cost;
            let quantity = listing.quantity;
            let totalCost = listing.total_cost;

            let row = {
                listingID: listingID,
                cost: cost,
                quantity: quantity,
                totalCost: totalCost
            };

            tableRows.push(row);
        }

        tableRows.sort((a, b) => a.cost - b.cost);

        // Create the table container
        let table = document.createElement('ul');
        table.id = 'hf-pmv-table';

        // Create the header row
        let headerRow = document.createElement('li');
        headerRow.className = 'hf-pmv-table-header';
        createPointsTableCell(headerRow, 'hf-pmv-cost', 'Cost');
        createPointsTableCell(headerRow, 'hf-pmv-qty', 'Quantity');
        createPointsTableCell(headerRow, 'hf-pmv-total', 'Total');
        table.appendChild(headerRow);

        // Loop through every listingID to fetch the necessary info
        for (let i = 0; i < tableRows.length; i++) {
            let row = tableRows[i];
            let numericListingID = parseInt(row.listingID);

            // Check if the listing ID is part of the personal listings fetched from the logs
            if (personalListings.includes(numericListingID)) {
                createPointsTableRow(table, 'self', row.listingID, row.cost, row.quantity, row.totalCost);
            } else {
                createPointsTableRow(table, 'other', row.listingID, row.cost, row.quantity, row.totalCost);
            }
        }

        contentWrapper.appendChild(table);
    }

    // Create a table row with the necessary data
    function createPointsTableRow(table, info, listingID, cost, quantity, totalCost) {
        let tableRow = document.createElement('li');
        tableRow.id = 'hf-pmv-' + listingID;
        tableRow.className = 'hf-pmv-table-row';

        let costText = cost.toLocaleString('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0, minimumFractionDigits: 0});
        createPointsTableCell(tableRow, 'hf-pmv-cost', costText)

        let quantityText = quantity.toLocaleString('en-US');
        createPointsTableCell(tableRow, 'hf-pmv-qty', quantityText)

        let totalCostText = totalCost.toLocaleString('en-US', {style: 'currency', currency: 'USD', maximumFractionDigits: 0, minimumFractionDigits: 0});
        createPointsTableCell(tableRow, 'hf-pmv-total', totalCostText)

        table.appendChild(tableRow);

        if (info === 'self') {
            tableRow.style.background = 'rgba(85,137,33,.5)';
        }
    }

    // Create a table cell with the necessary data
    function createPointsTableCell(tableRow, className, textContent) {
        let span = document.createElement('span');
        span.className = className;
        span.textContent = textContent;
        tableRow.appendChild(span);
    }

    // Add a refresh button
    function addRefreshButton() {
        let existingButton = document.getElementById('hf-refresh');
        if (existingButton) return;

        let timer = document.getElementById('hf-refresh-timer');
        if (timer) timer.remove();

        let title = document.getElementById('hf-pmv-title');

        let refreshButton = document.createElement('button');
        refreshButton.id = 'hf-refresh';
        refreshButton.style.float = 'right';
        refreshButton.innerHTML = `<svg class="hf-refresh-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 14.47"><defs><style>.cls-1{opacity:0.35;}.cls-2{fill:#fff;}.cls-3{fill:#777;}</style></defs><g id="Слой_2" data-name="Слой 2"><g id="icons"><g class="cls-1"><path class="cls-2" d="M1.68,7.74A5.05,5.05,0,0,1,11.5,6.05H8.42l3.84,4L16,6.05H13.26A6.74,6.74,0,1,0,11,13l-1.06-1.3A5.06,5.06,0,0,1,1.68,7.74"></path></g><path class="cls-3" d="M1.68,6.74A5.05,5.05,0,0,1,11.5,5.05H8.42l3.84,4L16,5.05H13.26A6.74,6.74,0,1,0,11,12l-1.06-1.3A5.06,5.06,0,0,1,1.68,6.74"></path></g></g></svg>`;

        title.appendChild(refreshButton);

        // Add event listener to the button
        refreshButton.addEventListener('click', function() {
            location.reload();
        });

        let cls2 = refreshButton.querySelector('.cls-2');
        let cls3 = refreshButton.querySelector('.cls-3');

        // Add event listeners for hover
        refreshButton.addEventListener('mouseenter', function() {
            refreshButton.style.color = 'var(--default-blue-color)';
            cls2.style.fill = 'var(--default-blue-color)';
            cls3.style.fill = 'var(--default-blue-color)';
        });

        refreshButton.addEventListener('mouseleave', function() {
            refreshButton.style.color = 'var(--default-color)';
            cls2.style.fill = 'var(--default-color)';
            cls3.style.fill = 'var(--default-color)';
        });
    }

    // Add a timer to count down to refresh time
    function addTimer() {
        let refreshTimestamp = timestamp + 30;

        let intervalID = setInterval(() => {
            let currentTimestamp = Math.floor(Date.now() / 1000);
            let remainingTime = refreshTimestamp - currentTimestamp;

            if (remainingTime <= 0) {
                clearInterval(intervalID);
                addRefreshButton();
                return;
            }

            let title = document.getElementById('hf-pmv-title');
            let timer = document.getElementById('hf-refresh-timer');
            if (!timer) {
                timer = document.createElement('span');
                timer.id = 'hf-refresh-timer';
            }

            timer.textContent = remainingTime;

            title.appendChild(timer);

        }, 1000);
    }



    // BAZAAR AND MARKET //

    function bazaarPage() {
        let contentWrapper = document.querySelector('.content-wrapper');
        if (!contentWrapper) return;

        let messageContent = document.querySelector('.msg.right-round');
        let originalText = messageContent.textContent;

        if (messageContent && (messageContent.textContent.includes('unavailable') || messageContent.textContent.includes('not available'))) {
            let newText = `${userName}'s bazaar is fetched by the "View Listings Anytime" script" with the help of the API!`;
            messageContent.textContent = newText;

            fetchBazaarData();
            createAPIlink('remove');
        }
    }

    function marketPage() {
        let contentWrapper = document.querySelector('.content-wrapper');
        if (!contentWrapper) return;

        let messageContent = document.querySelector('.msg.right-round');
        let originalText = messageContent.textContent;

        if (messageContent && (messageContent.textContent.includes('unavailable') || messageContent.textContent.includes('not available'))) {
            let newText = `Your item market listings are fetched by the "View Listings Anytime" script" with the help of the API!`;
            messageContent.textContent = newText;

            fetchMarket(null, null, null, true);
            createAPIlink('remove');
        }
    }

    function createBazaarTable(data, market) {
        let contentWrapper = document.querySelector('.content-wrapper');

        let tableDiv = document.createElement('div');
        if (!market) tableDiv.style.paddingTop = '16px';
        if (market) tableDiv.style.marginTop = '-8px';

        // Scrollable wrapper
        let scrollWrapper = document.createElement('div');
        scrollWrapper.style.overflowX = 'auto';
        scrollWrapper.style.display = 'block';

        let table = document.createElement('table');
        table.style.margin = '0 auto';
        table.style.background = 'var(--default-bg-panel-color)';
        table.style.width = '100%';
        table.style.minWidth = '800px'; // Optional: force scroll on smaller screens

        let thead = document.createElement('thead');
        let tbody = document.createElement('tbody');
        tbody.style.borderRadius = '5px';

        // Create table headers
        let headers = ['Image', 'Name', 'Bonus', 'Stock', 'Price each', 'Price total', 'Lowest price in market'];
        let headerRow = document.createElement('tr');
        headers.forEach(function(header) {
            let th = document.createElement('th');
            th.style.padding = '4px';
            th.textContent = header;
            th.style.background = 'var(--tabs-active-bg-gradient)';
            th.style.color = 'var(--default-color)';
            th.style.fontWeight = 'bold';
            th.style.textAlign = 'center';
            th.style.padding = '8px 4px';
            th.style.borderBottom = '1px solid grey';
            th.style.borderBottomColor = 'var(--default-panel-divider-outer-side-color)';
            headerRow.appendChild(th);
        });
        thead.appendChild(headerRow);
        table.appendChild(thead);

        // Create table body
        data.forEach(function(item, index) {
            let row = document.createElement('tr');
            row.style.borderBottom = '1px solid grey';
            row.style.borderBottomColor = 'var(--default-panel-divider-outer-side-color)';

            if (market) {
                item.ID = item.item.id;
                item.UID = item.item.uid;
                item.name = item.item.name;
                item.quantity = item.amount;
                item.price = item.price;
            }

            let itemID = item.ID;
            let itemUID = item.UID;
            let bonusText = '';

            if (itemUID) {
                itemUIDs.push({ uid: itemUID, index: index });
                bonusText = 'Loading...';
            }

            itemIDs.push({ id: itemID, index: index });

            createBazaarCell('Image', row, itemID);
            createBazaarCell(item.name, row);
            createBazaarCell(bonusText, row);
            createBazaarCell(item.quantity, row);
            createBazaarCell(item.price.toLocaleString('en-US', {
                style: 'currency',
                currency: 'USD',
                maximumFractionDigits: 0,
                minimumFractionDigits: 0
            }), row);
            createBazaarCell((item.quantity * item.price).toLocaleString('en-US', {
                style: 'currency',
                currency: 'USD',
                maximumFractionDigits: 0,
                minimumFractionDigits: 0
            }), row);
            createBazaarCell('Loading...', row);

            tbody.appendChild(row);
        });

        table.appendChild(tbody);
        scrollWrapper.appendChild(table);
        tableDiv.appendChild(scrollWrapper);
        contentWrapper.appendChild(tableDiv);

        fetchMarketDataForItems();
        fetchItemDetailsForUIDs();
    }

    function createBazaarCell(text, row, itemID) {
        let cell = document.createElement('td');

        if (text == 'Image') {
            let img = document.createElement('img');
            img = document.createElement('img');
            img.src = `/images/items/${itemID}/large.png`;
            img.srcset = `/images/items/${itemID}/large.png 1x, /images/items/${itemID}/[email protected] 2x, /images/items/${itemID}/[email protected] 3x, /images/items/${itemID}/[email protected] 4x`;
            img.alt = 'Item Image';
            img.style.height = '25px';
            cell.appendChild(img);
        } else {
            cell.textContent = text;
        }

        cell.style.color = 'var(--default-color)';
        cell.style.textAlign = 'center';
        cell.style.verticalAlign = 'middle';
        cell.style.padding = '4px';

        row.appendChild(cell);
    }


    function fetchMarketDataForItems() {
        let itemsToFetch = Math.min(maximumCalls, itemIDs.length);

        for (let i = 0; i < itemsToFetch; i++) {
            let { id, index } = itemIDs[i];
            fetchMarketData(id, index);
        }

        if (itemIDs.length > maximumCalls) {
            setTimeout(function () {
                fetchMarketDataForItems();
            }, 60000);
        }
    }

    function updateLowestPrice(data, index, itemID) {
        let lowestPrice = data.itemmarket.listings[0].price;

        // Update table with lowest bazaar price
        let table = document.querySelector('table');
        let cell = table.rows[index + 1].cells[6];
        cell.textContent = lowestPrice.toLocaleString('en-US', {style: 'currency', currency: 'USD', maximumFractiondigits: 0, minimumFractionDigits: 0});

        // Remove from itemIDs
        itemIDs = itemIDs.filter(item => item.id !== itemID);
    }

    function fetchItemDetailsForUIDs() {
        let itemsToFetch = Math.min(maximumCalls, itemUIDs.length);

        for (let i = 0; i < itemsToFetch; i++) {
            let { uid, index } = itemUIDs[i];
            fetchItemDetails(uid, index);
        }

        if (itemIDs.length > maximumCalls) {
            setTimeout(function () {
                fetchItemDetailsForUIDs();
            }, 60000);
        }
    }

    function addBonusInfo(data, index, itemUID) {
        let rarity = data.itemdetails.rarity;
        if (rarity == 'None') rarity = '';

        let quality = data.itemdetails.quality;
        let bonuses = data.itemdetails.bonuses;

        let bonusText = '';

        if (bonuses) {
            if (Object.keys(bonuses).length === 1) {
                // If there is only one bonus
                let bonus = bonuses[Object.keys(bonuses)[0]];
                bonusText = `<p style="padding:4px">${bonus.value}% ${bonus.bonus}</p>`;
            } else if (Object.keys(bonuses).length === 2) {
                // If there are two bonuses
                Object.keys(bonuses).forEach(key => {
                    let bonus = bonuses[key];
                    bonusText += `<p style="padding:4px">${bonus.value}% ${bonus.bonus}</p>`;
                });
            }
        }

        // Update table with bonus text
        let table = document.querySelector('table');
        let cell = table.rows[index + 1].cells[2];
        cell.innerHTML = `<p style="padding:4px">${rarity}<p>${quality} Quality</p>${bonusText}`;

        // Remove from itemUIDs
        itemUIDs = itemUIDs.filter(item => item.uid !== itemUID);
    }



    // API FETCHERS //

    async function fetchBazaar(ul, refreshed, mobile) {
        let apiUrl = `https://api.torn.com/user/?selections=bazaar&key=${apiKey}&comment=ViewListingsAnytime`;

        fetch(apiUrl)
            .then(response => response.json())
            .then(data => {
            let bazaar = data.bazaar;

            if (mobile) {
                createMobileList('Bazaar', ul, bazaar, refreshed);
            } else {
                createList('Bazaar', ul, bazaar, refreshed);
            }
        })
            .catch(error => console.error('Error fetching data: ' + error));

    }

    async function fetchMarket(ul, refreshed, mobile, page) {
        let apiUrl = `https://api.torn.com/v2/user/itemmarket?offset=0&key=${apiKey}&comment=ViewListingsAnytime`;

        fetch(apiUrl)
            .then(response => response.json())
            .then(data => {
            let itemmarket = data.itemmarket;

            if (page) {
                createBazaarTable(itemmarket, true);
                apiCallCount++;
            }

            if (mobile) {
                createMobileList('Market', ul, itemmarket, refreshed);
            } else {
                createList('Market', ul, itemmarket, refreshed);
            }
        })
            .catch(error => console.error('Error fetching data: ' + error));
    }

    function fetchHospitalTime() {
        let messageContainer = document.body.querySelector('.info-msg');
        let message = messageContainer.querySelector('.msg');
        if (!message) return;

        let apiUrl = `https://api.torn.com/user/?selections=basic&key=${apiKey}&comment=ViewPointsMarketAnytime`;

        fetch(apiUrl)
            .then(response => response.json())
            .then(data => {
            changeTimer(data, messageContainer, message)
        })
            .catch(error => console.error('Error fetching data: ' + error));
    }

    function fetchLogs() {
        let apiUrl = `https://api.torn.com/user/?key=${apiKey}&selections=log&log=5000&comment=ViewListingsAnytime`;

        fetch(apiUrl)
            .then(response => response.json())
            .then(data => {
            let logData = data.log;

            for (let logID in logData) {
                let log = logData[logID];
                let listingID = log.data.listing_id;

                personalListings.push(listingID);
            }

            fetchPointsMarket();
        })
            .catch(error => console.error('Error fetching data: ' + error));
    }

    function fetchPointsMarket() {
        let apiUrl = `https://api.torn.com/market/?selections=pointsmarket,timestamp&key=${apiKey}&comment=ViewPointsMarketAnytime`;

        fetch(apiUrl)
            .then(response => response.json())
            .then(data => {
            let pointsMarket = data.pointsmarket;
            timestamp = data.timestamp;

            addTimer();
            createPointsTable(pointsMarket);
        })
            .catch(error => console.error('Error fetching data: ' + error));
    }

    function findUsername() {
        let url = new URL(window.location.href);
        userID = url.searchParams.get('userId');

        let apiUrl = `https://api.torn.com/user/${userID}?key=${apiKey}&selections=basic&comment=ViewBazaarAnytime`;

        fetch(apiUrl)
            .then(response => response.json())
            .then(data => {
            userName = data.name;
        })
            .catch(error => console.error('Error fetching data: ' + error));
    }

    function fetchBazaarData() {
        let apiUrl = `https://api.torn.com/user/${userID}?key=${apiKey}&selections=bazaar&comment=ViewListingsAnytime`;

        fetch(apiUrl)
            .then(response => response.json())
            .then(data => {
            createBazaarTable(data.bazaar);
            apiCallCount++;
        })
            .catch(error => console.error('Error fetching data: ' + error));
    }

    function fetchOwnMarketData() {
        let apiUrl = `https://api.torn.com/v2/user/itemmarket?offset=0&key=${apiKey}&comment=ViewListingsAnytime`;

        fetch(apiUrl)
            .then(response => response.json())
            .then(data => {
            createBazaarTable(data.itemmarket, true);
            apiCallCount++;
        })
            .catch(error => console.error('Error fetching data: ' + error));
    }

    function fetchMarketData(itemID, index) {
        let apiUrl = `https://api.torn.com/v2/market/${itemID}/itemmarket?offset=0&key=${apiKey}&comment=ViewBazaarAnytime`

        // Make the API call
        fetch(apiUrl)
            .then(response => response.json())
            .then(data => {
            if (data.itemmarket && data.itemmarket.listings && data.itemmarket.listings.length > 0) {
                updateLowestPrice(data, index, itemID)
            } else {
                throw new Error('No items found in the bazaar');
            }
        })
            .catch(error => console.error('Error fetching data: ' + error));
    }

    function fetchItemDetails(itemUID, index) {
        let apiUrl = `https://api.torn.com/torn/${itemUID}?selections=itemdetails&key=${apiKey}&comment=ViewBazaarAnytime`;

        // Make the API call
        fetch(apiUrl)
            .then(response => response.json())
            .then(data => {
            if (data.itemdetails) {
                addBonusInfo(data, index, itemUID);
            } else {
                throw new Error('No item details found');
            }
        })
            .catch(error => console.error('Error fetching data: ' + error));
    }



    // HELPER FUNCTIONS //

    function createObserver(element) {
        let target;
        target = element;

        if (!target) {
            console.error(`[HF] Mutation Observer target not found.`);
            return;
        }

        let observer = new MutationObserver(function(mutationsList, observer) {
            for (let mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('menu-items')) {
                            createMobileMenu(node);
                        }
                    });
                }

                let config = { attributes: true, childList: true, subtree: true, characterData: true };
                observer.observe(target, config);
            }
        });

        let config = { attributes: true, childList: true, subtree: true, characterData: true };
        observer.observe(target, config);
    }


    function runScript() {
        if (listings) {
            createContainer('Bazaar');
            createContainer('Market');
        }

        if (window.location.href.includes('hf-viewlistings')) {
            changeMobilePage();
            if (apiKey) {
                createAPIlink('remove');
            } else {
                createAPIlink('add');
            }
        } else if (pointsmarket && window.location.href.includes('pmarket')) {
            pointsMarketPage();
        } else if (bazaar && window.location.href.includes('bazaar')) {
            findUsername();
            setTimeout(bazaarPage, 500);
        } else if (itemmarket && window.location.href.includes('ItemMarket')) {
            marketPage();
        }
    }

    runScript();

    // Attach click event listener to document body
    document.body.addEventListener('click', handleClick);

    // Function to run fetchItemQty() on click
    function handleClick(event) {
        runScript();
    }

    // STYLESHEET //

    GM_addStyle(`
        #hf-pmv-title {
            padding: 4px;
            color: var(--content-title-color);
            font-weight: 700;
            font-size: 22px;
            margin: 5px 0 -5px -4px
        }

        #hf-pmv-table {
            text-align: right;
            width: fit-content;
            margin: auto;
            color: var(--default-color);
            padding-top: 20px;
        }

        .hf-pmv-table-header {
            border-bottom: var(--default-panel-divider-outer-side-color) 1px solid;
            background: var(--title-black-gradient);
            border-radius: 5px 5px 0 0;
            width: fit-content;
            font-weight: bold;
            color: var(--tutorial-title-color);
        }

        .hf-pmv-table-row {
            border-bottom: var(--default-panel-divider-outer-side-color) 1px solid;
            border-top: var(--default-panel-divider-inner-side-color) 1px solid;
            background: var(--default-bg-panel-color);
            width: fit-content;
        }

        .hf-pmv-qty {
            border-left = 'var(--default-panel-divider-outer-side-color) 2px solid';
            border-right = 'var(--default-panel-divider-outer-side-color) 2px solid';
        }

        .hf-pmv-cost, .hf-pmv-qty {
            width: 100px;
            padding: 8px;
            display: inline-block;
        }

        .hf-pmv-total {
            width: 150px;
            padding: 8px;
            display: inline-block;
        }

        #hf-refresh {
            z-index: 9999;
            top: 188px;
            right: 475px;
            color: var(--default-color);
            cursor: pointer;
            width: 30px;
        }

        #hf-refresh-timer {
            float: right;
            font-size: 14px;
            margin-top: 6px;
            width: 30px;
            text-align: center;
        }

        .hf-refresh-svg {
            width: 14px
        }

        .hf-refresh-svg {
            margin-top: 4px;
        }

        @media only screen and (max-width: 785px) {
            .hf-pmv-total {
                width: 138px;
            }

            #hf-pmv-title {
                font-size: 14px;
            }

            .hf-refresh-svg, #hf-refresh-timer {
                margin-top: 0px;
            }

            #hf-refresh {
                margin-top: -2px;
            }
        }

        @media only screen and (max-width: 386px) {
            .hf-pmv-total {
               width: 100px;
            }

            .hf-pmv-cost, .hf-pmv-qty {
               width: 86px;
            }
        }
    `);

})();

QingJ © 2025

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