View Listings Anytime

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

})();