Bazaars in Item Market Powered by TornW3B

Displays bazaar listings with sorting controls

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bazaars in Item Market Powered by TornW3B
// @namespace    http://tampermonkey.net/
// @version      3.23
// @description  Displays bazaar listings with sorting controls
// @author       Weav3r
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// @match        https://www.torn.com/bazaar.php*
// @grant        GM.xmlHttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.deleteValue
// @grant        GM.listValues
// @connect      weav3r.dev
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    if (typeof GM_setValue === 'undefined' && typeof GM !== 'undefined') {
        const GM_getValue = function(key, defaultValue) {
            let value;
            try {
                value = localStorage.getItem('GMcompat_' + key);
                if (value !== null) {
                    return JSON.parse(value);
                }

                GM.getValue(key, defaultValue).then(val => {
                    if (val !== undefined) {
                        localStorage.setItem('GMcompat_' + key, JSON.stringify(val));
                    }
                });

                return defaultValue;
            } catch (e) {
                console.error('Error in GM_getValue compatibility:', e);
                return defaultValue;
            }
        };

        const GM_setValue = function(key, value) {
            try {
                // Store in both localStorage and GM.setValue
                localStorage.setItem('GMcompat_' + key, JSON.stringify(value));
                GM.setValue(key, value);
            } catch (e) {
                console.error('Error in GM_setValue compatibility:', e);
            }
        };

        const GM_deleteValue = function(key) {
            try {
                localStorage.removeItem('GMcompat_' + key);
                GM.deleteValue(key);
            } catch (e) {
                console.error('Error in GM_deleteValue compatibility:', e);
            }
        };

        const GM_listValues = function() {
            // This is an approximation - we can only list keys with our prefix
            const keys = [];
            try {
                for (let i = 0; i < localStorage.length; i++) {
                    const key = localStorage.key(i);
                    if (key.startsWith('GMcompat_')) {
                        keys.push(key.substring(9)); // Remove the prefix
                    }
                }
            } catch (e) {
                console.error('Error in GM_listValues compatibility:', e);
            }
            return keys;
        };

        window.GM_getValue = GM_getValue;
        window.GM_setValue = GM_setValue;
        window.GM_deleteValue = GM_deleteValue;
        window.GM_listValues = GM_listValues;
    }

    const CACHE_DURATION_MS = 60000,
          CARD_WIDTH = 180;

    let currentSortKey = "price",
        currentSortOrder = "asc",
        allListings = [],
        currentDarkMode = document.body.classList.contains('dark-mode'),
        currentItemName = "",
        displayMode = "percentage",
        isMobileView = false;

    const scriptSettings = {
        defaultSort: "price",
        defaultOrder: "asc",
        apiKey: "",
        listingFee: parseFloat(GM_getValue("bazaarListingFee") || "0"),
        defaultDisplayMode: "percentage",
        linkBehavior: GM_getValue("bazaarLinkBehavior") || "new_tab",
        layoutMode: "default",
    };

    const updateStyles = () => {
        let styleEl = document.getElementById('bazaar-enhanced-styles');

        if (!styleEl) {
            styleEl = document.createElement('style');
            styleEl.id = 'bazaar-enhanced-styles';
            document.head.appendChild(styleEl);
        }

        styleEl.textContent = `
            .bazaar-profit-tooltip {
                position: fixed;
                background: ${currentDarkMode ? '#333' : '#fff'};
                color: ${currentDarkMode ? '#fff' : '#333'};
                border: 1px solid ${currentDarkMode ? '#555' : '#ddd'};
                padding: 10px 14px;
                border-radius: 5px;
                box-shadow: 0 3px 10px rgba(0,0,0,0.3);
                z-index: 99999;
                min-width: 200px;
                max-width: 280px;
                width: auto;
                pointer-events: none;
                transition: opacity 0.2s ease;
                font-size: 13px;
                line-height: 1.4;
            }

            @media (max-width: 768px) {
                .bazaar-profit-tooltip {
                    font-size: 12px;
                    max-width: 260px;
                    padding: 8px 12px;
                }
            }
        `;
    };

    updateStyles();

    const darkModeObserver = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.attributeName === 'class') {
                const newDarkMode = document.body.classList.contains('dark-mode');
                if (newDarkMode !== currentDarkMode) {
                    currentDarkMode = newDarkMode;
                    updateStyles();
                }
            }
        });
    });
    darkModeObserver.observe(document.body, { attributes: true });

    function checkMobileView() {
        isMobileView = window.innerWidth < 784;
        return isMobileView;
    }
    checkMobileView();
    window.addEventListener('resize', function() {
        checkMobileView();
        processMobileSellerList();
    });

    function loadSettings() {
        try {
            const saved = GM_getValue("bazaarsSettings");
            if (saved) {
                const parsedSettings = JSON.parse(saved);

                Object.assign(scriptSettings, parsedSettings);

                if (parsedSettings.defaultSort) {
                    currentSortKey = parsedSettings.defaultSort;
                }
                if (parsedSettings.defaultOrder) {
                    currentSortOrder = parsedSettings.defaultOrder;
                }
                if (parsedSettings.defaultDisplayMode) {
                    displayMode = parsedSettings.defaultDisplayMode;
                }
            }
        } catch (e) {
            console.error("Oops, settings failed to load:", e);
        }
    }

    function saveSettings() {
        try {
            GM_setValue("bazaarsSettings", JSON.stringify(scriptSettings));
            GM_setValue("bazaarApiKey", scriptSettings.apiKey || "");
            GM_setValue("bazaarDefaultSort", scriptSettings.defaultSort || "price");
            GM_setValue("bazaarDefaultOrder", scriptSettings.defaultOrder || "asc");
            GM_setValue("bazaarListingFee", scriptSettings.listingFee || 0);
            GM_setValue("bazaarDefaultDisplayMode", scriptSettings.defaultDisplayMode || "percentage");
            GM_setValue("bazaarLinkBehavior", scriptSettings.linkBehavior || "new_tab");
            GM_setValue("bazaarLayoutMode", scriptSettings.layoutMode || "default");
        } catch (e) {
            console.error("Settings save hiccup:", e);
        }
    }
    loadSettings();

    const style = document.createElement("style");
    style.textContent = `
        .bazaar-button {
            padding: 3px 6px;
            border: 1px solid #ccc;
            border-radius: 4px;
            background-color: #fff;
            color: #000;
            cursor: pointer;
            font-size: 12px;
            margin-left: 4px;
        }
        .dark-mode .bazaar-button {
            border: 1px solid #444;
            background-color: #1a1a1a;
            color: #fff;
        }
        .bazaar-modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.7);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 99999;
        }
        .bazaar-info-container {
            font-size: 13px;
            border-radius: 4px;
            margin: 5px 0;
            padding: 10px;
            display: flex;
            flex-direction: column;
            gap: 8px;
            background-color: #f9f9f9;
            color: #000;
            border: 1px solid #ccc;
            box-sizing: border-box;
            width: 100%;
            overflow: hidden;
        }
        .dark-mode .bazaar-info-container {
            background-color: #2f2f2f;
            color: #ccc;
            border: 1px solid #444;
        }
        .bazaar-info-header {
            font-size: 16px;
            font-weight: bold;
            color: #000;
        }
        .dark-mode .bazaar-info-header {
            color: #fff;
        }
        .bazaar-sort-controls {
            display: flex;
            align-items: center;
            gap: 5px;
            font-size: 12px;
            padding: 5px;
            background-color: #eee;
            border-radius: 4px;
            border: 1px solid #ccc;
        }
        .dark-mode .bazaar-sort-controls {
            background-color: #333;
            border: 1px solid #444;
        }
        .bazaar-sort-select {
            padding: 3px 24px 3px 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
            background: #fff url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNiIgdmlld0JveD0iMCAwIDEwIDYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTAgMGw1IDYgNS02eiIgZmlsbD0iIzY2NiIvPjwvc3ZnPg==") no-repeat right 8px center;
            background-size: 10px 6px;
            -webkit-appearance: none;
            -moz-appearance: none;
            appearance: none;
            cursor: pointer;
        }
        .bazaar-profit-tooltip {
            position: fixed;
            background: #fff;
            color: #333;
            border: 1px solid #ddd;
            padding: 8px 12px;
            border-radius: 4px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.3);
            z-index: 99999;
            min-width: 200px;
            max-width: 280px;
            width: auto;
            pointer-events: none;
            transition: opacity 0.2s ease;
        }
        .dark-mode .bazaar-profit-tooltip {
            background: #333;
            color: #fff;
            border: 1px solid #555;
        }
        .dark-mode .bazaar-sort-select {
            border: 1px solid #444;
            background-color: #1a1a1a;
            color: #fff;
            background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iNiIgdmlld0JveD0iMCAwIDEwIDYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTAgMGw1IDYgNS02eiIgZmlsbD0iI2NjYyIvPjwvc3ZnPg==");
        }
        .bazaar-sort-select:focus {
            outline: none;
            border-color: #0078d7;
            box-shadow: 0 0 0 1px #0078d7;
        }
        .bazaar-min-qty {
            background-color: #fff;
            color: #000;
            font-size: 12px;
        }
        .dark-mode .bazaar-min-qty {
            border: 1px solid #444 !important;
            background-color: #1a1a1a;
            color: #fff;
        }
        .bazaar-min-qty:focus {
            outline: none;
            border-color: #0078d7 !important;
            box-shadow: 0 0 0 1px #0078d7;
        }
        .bazaar-scroll-container {
            position: relative;
            display: flex;
            align-items: stretch;
            width: 100%;
            box-sizing: border-box;
        }
        .bazaar-scroll-wrapper {
            flex: 1;
            overflow-x: auto;
            overflow-y: hidden;
            height: 100px;
            white-space: nowrap;
            padding-bottom: 3px;
            border-radius: 4px;
            border: 1px solid #ccc;
            margin: 0 auto;
            max-width: calc(100% - 30px);
            position: relative;
        }
        .dark-mode .bazaar-scroll-wrapper {
            border: 1px solid #444;
        }
        .bazaar-scroll-arrow {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 12px;
            flex-shrink: 0;
            flex-grow: 0;
            cursor: pointer;
            background-color: transparent;
            border: none;
            opacity: 0.5;
            transition: opacity 0.2s ease;
            margin: 0 1px;
            z-index: 2;
            position: relative;
        }
        .bazaar-scroll-arrow:hover {
            opacity: 0.9;
            background-color: transparent;
        }
        .dark-mode .bazaar-scroll-arrow {
            background-color: transparent;
            border: none;
        }
        .bazaar-scroll-arrow svg {
            width: 18px !important;
            height: 18px !important;
            color: #888;
        }
        .dark-mode .bazaar-scroll-arrow svg {
            color: #777;
        }
        .bazaar-scroll-arrow.left {
            padding-left: 10px;
            margin-left: -10px;
        }
        .bazaar-scroll-arrow.right {
            padding-right: 10px;
            margin-right: -10px;
        }
        .bazaar-scroll-wrapper::-webkit-scrollbar {
            height: 8px;
        }
        .bazaar-scroll-wrapper::-webkit-scrollbar-track {
            background: #f1f1f1;
        }
        .bazaar-scroll-wrapper::-webkit-scrollbar-thumb {
            background: #888;
            border-radius: 4px;
        }
        .bazaar-scroll-wrapper::-webkit-scrollbar-thumb:hover {
            background: #555;
        }
        .dark-mode .bazaar-scroll-wrapper::-webkit-scrollbar-track {
            background: #333;
        }
        .dark-mode .bazaar-scroll-wrapper::-webkit-scrollbar-thumb {
            background: #555;
        }
        .dark-mode .bazaar-scroll-wrapper::-webkit-scrollbar-thumb:hover {
            background: #777;
        }
        .bazaar-card-container {
            position: relative;
            height: 100%;
            display: flex;
            align-items: center;
        }
        .bazaar-listing-card {
            position: absolute;
            min-width: 140px;
            max-width: 200px;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            border-radius: 4px;
            padding: 8px;
            font-size: clamp(12px, 1vw, 14px);
            box-sizing: border-box;
            overflow: hidden;
            background-color: #fff;
            color: #000;
            border: 1px solid #ccc;
            top: 50%;
            transform: translateY(-50%);
            word-break: break-word;
            height: auto;
            /* Added transition for position, opacity and scale */
            transition: left 0.5s ease, opacity 0.5s ease, transform 0.5s ease;
        }
        .dark-mode .bazaar-listing-card {
            background-color: #1a1a1a;
            color: #fff;
            border: 1px solid #444;
        }
        /* Fade-in/out classes for animations */
        .fade-in {
            opacity: 0;
            transform: translateY(-50%) scale(0.8);
        }
        .fade-out {
            opacity: 0;
            transform: translateY(-50%) scale(0.8);
        }
        .bazaar-listing-footnote {
            font-size: 11px;
            text-align: right;
            color: #555;
        }
        .dark-mode .bazaar-listing-footnote {
            color: #aaa;
        }
        .bazaar-listing-source {
            font-size: 10px;
            text-align: right;
            color: #555;
        }
        .dark-mode .bazaar-listing-source {
            color: #aaa;
        }
        .bazaar-footer-container {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-top: 5px;
            font-size: 10px;
        }
        .bazaar-powered-by span {
            color: #999;
        }
        .dark-mode .bazaar-powered-by span {
            color: #666;
        }
        .bazaar-powered-by a {
            text-decoration: underline;
            color: #555;
        }
        .dark-mode .bazaar-powered-by a {
            color: #aaa;
        }
        @keyframes popAndFlash {
            0%   { transform: scale(1); background-color: rgba(0,255,0,0.6); }
            50%  { transform: scale(1.05); }
            100% { transform: scale(1); background-color: inherit; }
        }
        .pop-flash {
            animation: popAndFlash 0.8s ease-in-out forwards;
        }
        .green-outline {
            border: 3px solid green !important;
        }
        .bazaar-settings-modal {
            background-color: #fff;
            border-radius: 8px;
            padding: 24px;
            width: 500px;
            max-width: 95vw;
            max-height: 90vh;
            overflow-y: auto;
            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
            position: relative;
            z-index: 100000;
            font-family: 'Arial', sans-serif;
        }
        .dark-mode .bazaar-settings-modal {
            background-color: #2a2a2a;
            color: #e0e0e0;
            border: 1px solid #444;
        }
        .bazaar-settings-title {
            font-size: 20px;
            font-weight: bold;
            margin-bottom: 20px;
            color: #333;
        }
        .dark-mode .bazaar-settings-title {
            color: #fff;
        }
        .bazaar-tabs {
            display: flex;
            border-bottom: 1px solid #ddd;
            margin-bottom: 20px;
            padding-bottom: 0;
            flex-wrap: wrap;
        }
        .dark-mode .bazaar-tabs {
            border-bottom: 1px solid #444;
        }
        .bazaar-tab {
            padding: 10px 16px;
            cursor: pointer;
            margin-right: 5px;
            margin-bottom: 5px;
            border: 1px solid transparent;
            border-bottom: none;
            border-radius: 4px 4px 0 0;
            font-weight: normal;
            background-color: #f5f5f5;
            color: #555;
            position: relative;
            bottom: -1px;
        }
        .dark-mode .bazaar-tab {
            background-color: #333;
            color: #ccc;
        }
        .bazaar-tab.active {
            background-color: #fff;
            color: #333;
            border-color: #ddd;
            font-weight: bold;
            padding-bottom: 11px;
        }
        .dark-mode .bazaar-tab.active {
            background-color: #2a2a2a;
            color: #fff;
            border-color: #444;
        }
        .bazaar-tab-content {
            display: none;
        }
        .bazaar-tab-content.active {
            display: block;
        }
        .bazaar-settings-group {
            margin-bottom: 20px;
        }
        .bazaar-settings-item {
            margin-bottom: 18px;
        }
        .bazaar-settings-item label {
            display: block;
            margin-bottom: 8px;
            font-weight: bold;
            font-size: 14px;
        }
        .bazaar-settings-item input[type="text"],
        .bazaar-settings-item select,
        .bazaar-number-input {
            width: 100%;
            padding: 8px 12px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 14px;
            background-color: #fff;
            color: #333;
            max-width: 200px;
        }
        .dark-mode .bazaar-settings-item input[type="text"],
        .dark-mode .bazaar-settings-item select,
        .dark-mode .bazaar-number-input {
            border: 1px solid #444;
            background-color: #222;
            color: #e0e0e0;
        }
        .bazaar-settings-item select {
            max-width: 200px;
        }
        .bazaar-number-input {
            -moz-appearance: textfield;
            appearance: textfield;
            width: 60px !important;
        }
        .bazaar-number-input::-webkit-outer-spin-button,
        .bazaar-number-input::-webkit-inner-spin-button {
            -webkit-appearance: none;
            margin: 0;
        }
        .bazaar-api-note {
            font-size: 12px;
            margin-top: 6px;
            color: #666;
            line-height: 1.4;
        }
        .dark-mode .bazaar-api-note {
            color: #aaa;
        }
        .bazaar-script-item {
            margin-bottom: 16px;
            padding-bottom: 16px;
            border-bottom: 1px solid #eee;
        }
        .dark-mode .bazaar-script-item {
            border-bottom: 1px solid #333;
        }
        .bazaar-script-item:last-child {
            border-bottom: none;
        }
        .bazaar-script-name {
            font-weight: bold;
            font-size: 16px;
            margin-bottom: 5px;
        }
        .bazaar-script-desc {
            margin-bottom: 8px;
            line-height: 1.4;
            color: #555;
        }
        .dark-mode .bazaar-script-desc {
            color: #bbb;
        }
        .bazaar-script-link {
            display: inline-block;
            margin-top: 5px;
            color: #2196F3;
            text-decoration: none;
        }
        .bazaar-script-link:hover {
            text-decoration: underline;
        }
        .bazaar-changelog {
            margin-bottom: 20px;
        }
        .bazaar-changelog-version {
            font-weight: bold;
            margin-bottom: 8px;
            font-size: 15px;
        }
        .bazaar-changelog-date {
            font-style: italic;
            color: #666;
            font-size: 13px;
            margin-bottom: 5px;
        }
        .dark-mode .bazaar-changelog-date {
            color: #aaa;
        }
        .bazaar-changelog-list {
            margin-left: 20px;
            margin-bottom: 15px;
        }
        .bazaar-changelog-item {
            margin-bottom: 5px;
            line-height: 1.4;
        }
        .bazaar-credits {
            margin-top: 20px;
            padding-top: 15px;
            border-top: 1px solid #eee;
        }
        .dark-mode .bazaar-credits {
            border-top: 1px solid #444;
        }
        .bazaar-credits h3 {
            font-size: 16px;
            margin-bottom: 10px;
        }
        .bazaar-credits p {
            line-height: 1.4;
            margin-bottom: 8px;
        }
        .bazaar-provider {
            font-weight: bold;
        }
        .bazaar-settings-buttons {
            display: flex;
            justify-content: flex-end;
            gap: 12px;
            margin-top: 30px;
        }
        .bazaar-settings-save,
        .bazaar-settings-cancel {
            padding: 8px 16px;
            border: none;
            border-radius: 4px;
            font-size: 14px;
            cursor: pointer;
            font-weight: bold;
        }
        .bazaar-settings-save {
            background-color: #4CAF50;
            color: white;
        }
        .bazaar-settings-save:hover {
            background-color: #45a049;
        }
        .bazaar-settings-cancel {
            background-color: #f5f5f5;
            color: #333;
            border: 1px solid #ddd;
        }
        .dark-mode .bazaar-settings-cancel {
            background-color: #333;
            color: #e0e0e0;
            border: 1px solid #444;
        }
        .bazaar-settings-cancel:hover {
            background-color: #e9e9e9;
        }
        .dark-mode .bazaar-settings-cancel:hover {
            background-color: #444 !important;
            border-color: #555 !important;
        }
        .bazaar-settings-footer {
            margin-top: 20px;
            font-size: 12px;
            color: #777;
            text-align: center;
            padding-top: 15px;
            border-top: 1px solid #eee;
        }
        .dark-mode .bazaar-settings-footer {
            color: #999;
            border-top: 1px solid #444;
        }
        .bazaar-settings-footer a {
            color: #2196F3;
            text-decoration: none;
        }
        .bazaar-settings-footer a:hover {
            text-decoration: underline;
        }
        @media (max-width: 600px) {
            .bazaar-settings-modal {
                padding: 16px;
                width: 100%;
                max-width: 100%;
                border-radius: 0;
                max-height: 100vh;
            }
            .bazaar-settings-title {
                font-size: 18px;
                margin-bottom: 16px;
            }
            .bazaar-tab {
                padding: 8px 12px;
                font-size: 14px;
            }
            .bazaar-settings-item label {
                font-size: 13px;
            }
            .bazaar-settings-item input[type="text"],
            .bazaar-settings-item select,
            .bazaar-number-input {
                padding: 6px 10px;
                font-size: 13px;
                max-width: 100%;
            }
            .bazaar-settings-item {
                margin-bottom: 14px;
            }
            .bazaar-settings-save,
            .bazaar-settings-cancel {
                padding: 6px 12px;
                font-size: 13px;
            }
            .bazaar-api-note {
                font-size: 11px;
            }
            .bazaar-settings-buttons {
                margin-top: 20px;
            }
            .bazaar-settings-footer {
                font-size: 11px;
            }
        }

        /* compact listing mode */
        .bazaar-info-container.compact-layout .bazaar-scroll-arrow {
            display: none;
        }
        .bazaar-info-container.compact-layout .bazaar-scroll-wrapper {
            overflow-x: hidden;
            overflow-y: auto;
            max-height: 200px;
            height: auto;
            white-space: normal;
            padding: 5px;
            width: 100%;
            max-width: 100%;
        }
        .bazaar-info-container.compact-layout .bazaar-card-container {
            flex-direction: column;
            align-items: stretch;
            gap: 6px;
            height: auto;
        }
        .bazaar-info-container.compact-layout .bazaar-listing-card {
            position: static;
            display: flex;
            flex-direction: row;
            flex-wrap: wrap;
            align-items: center;
            gap: 12px;
            padding: 6px 10px;
            width: 100%;
            max-width: none;
            transform: none;
            justify-content: flex-start;
        }
        .bazaar-info-container.compact-layout .card-info-item {
            white-space: nowrap;
        }
        .bazaar-info-container.compact-layout .bazaar-player-link {
            font-weight: bold;
            text-decoration: underline;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            max-width: 150px;
        }
        .bazaar-info-container.compact-layout .bazaar-listing-footnote {
            margin-left: auto;
            text-align: left;
        }
    `;
    document.head.appendChild(style);

    function fetchJSON(url, callback) {
        let retryCount = 0;
        const MAX_RETRIES = 2;
        const TIMEOUT_MS = 10000;
        const RETRY_DELAY_MS = 2000;

        function makeRequest(options) {
            if (typeof GM_xmlhttpRequest !== 'undefined') {
                return GM_xmlhttpRequest(options);
            } else if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest !== 'undefined') {
                return GM.xmlHttpRequest(options);
            } else {
                console.error('Neither GM_xmlhttpRequest nor GM.xmlHttpRequest are available');
                options.onerror && options.onerror(new Error('XMLHttpRequest API not available'));
                return null;
            }
        }

        function attemptFetch() {
            let timeoutId = setTimeout(() => {
                console.warn(`Request to ${url} timed out, ${retryCount < MAX_RETRIES ? 'retrying...' : 'giving up.'}`);
                if (retryCount < MAX_RETRIES) {
                    retryCount++;
                    setTimeout(attemptFetch, RETRY_DELAY_MS);
                } else {
                    callback(null);
                }
            }, TIMEOUT_MS);

            makeRequest({
                method: 'GET',
                url,
                timeout: TIMEOUT_MS,
                onload: res => {
                    clearTimeout(timeoutId);
                    try {
                        if (res.status >= 200 && res.status < 300) {
                            callback(JSON.parse(res.responseText));
                        } else {
                            console.warn(`Request to ${url} failed with status ${res.status}`);
                            if (retryCount < MAX_RETRIES) {
                                retryCount++;
                                setTimeout(attemptFetch, RETRY_DELAY_MS);
                            } else {
                                callback(null);
                            }
                        }
                    } catch (e) {
                        console.error(`Error parsing response from ${url}:`, e);
                        callback(null);
                    }
                },
                onerror: (error) => {
                    clearTimeout(timeoutId);
                    console.warn(`Request to ${url} failed:`, error);
                    if (retryCount < MAX_RETRIES) {
                        retryCount++;
                        setTimeout(attemptFetch, RETRY_DELAY_MS);
                    } else {
                        callback(null);
                    }
                },
                ontimeout: () => {
                    clearTimeout(timeoutId);
                    console.warn(`Request to ${url} timed out natively`);
                    if (retryCount < MAX_RETRIES) {
                        retryCount++;
                        setTimeout(attemptFetch, RETRY_DELAY_MS);
                    } else {
                        callback(null);
                    }
                }
            });
        }
        attemptFetch();
    }

    let cachedItemsData = null;
    function getStoredItems() {
        if (cachedItemsData === null) {
            try {
                cachedItemsData = JSON.parse(GM_getValue("tornItems") || "{}");
            } catch (e) {
                cachedItemsData = {};
                console.error("Stored items got funky:", e);
            }
        }
        return cachedItemsData;
    }

    function getCache(itemId) {
        try {
            const key = "tornBazaarCache_" + itemId,
                  cached = GM_getValue(key);
            if (cached) {
                const payload = JSON.parse(cached);
                if (Date.now() - payload.timestamp < CACHE_DURATION_MS) return payload.data;
            }
        } catch (e) {}
        return null;
    }

    function setCache(itemId, data) {
        try {
            GM_setValue("tornBazaarCache_" + itemId, JSON.stringify({ timestamp: Date.now(), data }));
        } catch (e) {}
    }

    function getRelativeTime(ts) {
        const diffSec = Math.floor((Date.now() - ts * 1000) / 1000);
        if (diffSec < 60) return diffSec + 's ago';
        if (diffSec < 3600) return Math.floor(diffSec / 60) + 'm ago';
        if (diffSec < 86400) return Math.floor(diffSec / 3600) + 'h ago';
        return Math.floor(diffSec / 86400) + 'd ago';
    }

    const svgTemplates = {
        rightArrow: `<svg viewBox="0 0 320 512"><path fill="currentColor" d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"/></svg>`,
        leftArrow: `<svg viewBox="0 0 320 512"><path fill="currentColor" d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"/></svg>`,
        warningIcon: `<path fill="currentColor" d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/>`,
        infoIcon: `<path fill="currentColor" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/>`
    };

    function createListingCard(listing, index) {
        const card = document.createElement('div');
        card.className = 'bazaar-listing-card';
        card.dataset.index = index;
        const listingKey = listing.player_id + '-' + listing.price + '-' + listing.quantity;
        card.dataset.listingKey = listingKey;
        card.dataset.quantity = listing.quantity;

        let visitedColor = '#00aaff';
        try {
            const key = `visited_${listing.item_id}_${listing.player_id}`;
            const data = JSON.parse(GM_getValue(key));
            if (data && data.lastClickedUpdated >= listing.updated) {
                visitedColor = 'purple';
            }
        } catch (e) {}

        const displayName = listing.player_name || `ID: ${listing.player_id}`;
        const priceComparisonHTML = getPriceComparisonHtml(listing.price, listing.quantity);

        if (scriptSettings.layoutMode === 'compact') {
            card.innerHTML = `
                <a href="https://www.torn.com/bazaar.php?userId=${listing.player_id}&itemId=${listing.item_id}&highlight=1#/"
                    data-visited-key="visited_${listing.item_id}_${listing.player_id}"
                    data-updated="${listing.updated}"
                    ${scriptSettings.linkBehavior === 'new_tab' ? 'target="_blank" rel="noopener noreferrer"' : ''}
                    class="bazaar-player-link" style="color:${visitedColor};">
                    ${displayName}
                </a>
                <div class="card-info-item"><strong>Price:</strong> <span>$${listing.price.toLocaleString()}</span></div>
                <div class="card-info-item"><strong>Qty:</strong> <span>${listing.quantity}</span></div>
                ${priceComparisonHTML}
                <div class="bazaar-listing-footnote">${getRelativeTime(listing.updated)}</div>
            `;
        } else {
            card.style.position = "absolute";
            card.style.left = (index * CARD_WIDTH) + "px";
            card.style.width = CARD_WIDTH + "px";

            card.innerHTML = `
                <div>
                    <div style="display:flex; align-items:center; gap:5px; margin-bottom:6px; flex-wrap:wrap">
                        <a href="https://www.torn.com/bazaar.php?userId=${listing.player_id}&itemId=${listing.item_id}&highlight=1#/"
                            data-visited-key="visited_${listing.item_id}_${listing.player_id}"
                            data-updated="${listing.updated}"
                            ${scriptSettings.linkBehavior === 'new_tab' ? 'target="_blank" rel="noopener noreferrer"' : ''}
                            style="font-weight:bold; color:${visitedColor}; text-decoration:underline;">
                            Player: ${displayName}
                        </a>
                    </div>
                    <div>
                        <div style="margin-bottom:2px">
                            <strong>Price:</strong> <span style="word-break:break-all;">$${listing.price.toLocaleString()}</span>
                        </div>
                        <div style="display:flex; align-items:center">
                            <strong>Qty:</strong><span style="margin-left:4px">${listing.quantity}</span>
                            <span style="margin-left:auto">${priceComparisonHTML}</span>
                        </div>
                    </div>
                </div>
                <div style="margin-top:6px">
                    <div class="bazaar-listing-footnote">Updated: ${getRelativeTime(listing.updated)}</div>
                </div>
            `;
        }

        const playerLink = card.querySelector('a');
        playerLink.addEventListener('click', (e) => {
            GM_setValue(playerLink.dataset.visitedKey, JSON.stringify({ lastClickedUpdated: listing.updated }));
            playerLink.style.color = 'purple';
            const behavior = scriptSettings.linkBehavior || 'new_tab';
            if (behavior !== 'same_tab') {
                e.preventDefault();
                if (behavior === 'new_window') {
                    window.open(playerLink.href, '_blank', 'noopener,noreferrer,width=1200,height=800');
                } else {
                    window.open(playerLink.href, '_blank', 'noopener,noreferrer');
                }
            }
        });

        const priceComparison = card.querySelector('.bazaar-price-comparison');
        if (priceComparison) {
            const tooltip = document.createElement('div');
            tooltip.className = 'bazaar-profit-tooltip';
            tooltip.style.display = 'none';
            tooltip.style.opacity = '0';
            tooltip.innerHTML = priceComparison.getAttribute('data-tooltip');

            priceComparison.addEventListener('mouseenter', e => {
                document.body.appendChild(tooltip);
                tooltip.style.display = 'block';

                // Position initially to measure size
                tooltip.style.left = '0';
                tooltip.style.top = '0';

                // Get dimensions after adding to DOM
                const rect = e.target.getBoundingClientRect();
                const tooltipRect = tooltip.getBoundingClientRect();

                // Calculate optimal position
                let left = rect.left;
                let top = rect.bottom + 5;

                // Check horizontal overflow
                if (left + tooltipRect.width > window.innerWidth) {
                    left = Math.max(5, window.innerWidth - tooltipRect.width - 5);
                }

                // Check vertical overflow and place above if needed
                if (top + tooltipRect.height > window.innerHeight) {
                    top = Math.max(5, rect.top - tooltipRect.height - 5);
                }

                // Apply final position
                tooltip.style.left = left + 'px';
                tooltip.style.top = top + 'px';

                // Fade in
                requestAnimationFrame(() => {
                    tooltip.style.opacity = '1';
                });
            });

            priceComparison.addEventListener('mouseleave', () => {
                tooltip.style.opacity = '0';
                // Remove after transition
                setTimeout(() => {
                    if (tooltip.parentNode) tooltip.parentNode.removeChild(tooltip);
                }, 200);
            });
        }

        return card;
    }

    function getPriceComparisonHtml(listingPrice, quantity) {
        try {
            const stored = getStoredItems();
            const match = Object.values(stored).find(item =>
                                                     item.name && item.name.toLowerCase() === currentItemName.toLowerCase());
            if (match && match.market_value) {
                const marketValue = Number(match.market_value),
                      priceDiff = listingPrice - marketValue,
                      percentDiff = ((listingPrice / marketValue) - 1) * 100,
                      listingFee = scriptSettings.listingFee || 0,
                      totalCost = listingPrice * quantity,
                      potentialRevenue = marketValue * quantity,
                      feeAmount = Math.ceil(potentialRevenue * (listingFee / 100)),
                      potentialProfit = potentialRevenue - totalCost - feeAmount,
                      minResellPrice = Math.ceil(listingPrice / (1 - (listingFee / 100)));

                let color, text;
                const absProfit = Math.abs(potentialProfit);
                let abbrevValue = potentialProfit < 0 ? '-' : '';
                if (absProfit >= 1000000) {
                    abbrevValue += '$' + (absProfit / 1000000).toFixed(1).replace(/\.0$/, '') + 'm';
                } else if (absProfit >= 1000) {
                    abbrevValue += '$' + (absProfit / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
                } else {
                    abbrevValue += '$' + absProfit;
                }
                if (potentialProfit > 0) {
                    color = currentDarkMode ? '#7fff7f' : '#006400';
                    text = displayMode === "percentage" ? `(${percentDiff.toFixed(1)}%)` : `(${abbrevValue})`;
                } else if (potentialProfit < 0) {
                    color = currentDarkMode ? '#ff7f7f' : '#8b0000';
                    text = displayMode === "percentage" ? `(+${percentDiff.toFixed(1)}%)` : `(${abbrevValue})`;
                } else {
                    color = currentDarkMode ? '#cccccc' : '#666666';
                    text = displayMode === "percentage" ? `(0%)` : `($0)`;
                }

                // Improved tooltip content focusing only on key information
                const tooltipContent = `
                    <div style="font-weight:bold; font-size:13px; margin-bottom:6px; text-align:center;">
                        ${potentialProfit >= 0 ? 'PROFIT' : 'LOSS'}: ${potentialProfit >= 0 ? '$' : '-$'}${Math.abs(potentialProfit).toLocaleString()}
                    </div>
                    <hr style="margin: 4px 0; border-color: ${currentDarkMode ? '#444' : '#ddd'}">
                    <div>Total Cost: $${totalCost.toLocaleString()} (${quantity} item${quantity > 1 ? 's' : ''})</div>
                    ${listingFee > 0 ? `<div>Resale Fee: ${listingFee}% ($${feeAmount.toLocaleString()})</div>` : ''}
                    ${listingFee > 0 ? `<div style="margin-top:6px; font-weight:bold;">Min. Resell Price: $${minResellPrice.toLocaleString()}</div>` : ''}
                `;
                const span = document.createElement('span');
                span.style.fontWeight = 'bold';
                span.style.fontSize = '10px';
                span.style.padding = '0 4px';
                span.style.borderRadius = '2px';
                span.style.color = color;
                span.style.cursor = 'help';
                span.style.whiteSpace = 'nowrap';
                span.textContent = text;
                span.className = 'bazaar-price-comparison';
                span.setAttribute('data-tooltip', tooltipContent);
                return span.outerHTML;
            }
        } catch (e) {
            console.error("Price comparison error:", e);
        }
        return '';
    }

    function renderVirtualCards(infoContainer) {
        const cardContainer = infoContainer.querySelector('.bazaar-card-container'),
              scrollWrapper = infoContainer.querySelector('.bazaar-scroll-wrapper');
        if (!cardContainer || !scrollWrapper || !infoContainer.isConnected) return;
        try {
            const minQtyInput = infoContainer.querySelector('.bazaar-min-qty');
            const minQty = minQtyInput && minQtyInput.value ? parseInt(minQtyInput.value, 10) : 0;
            if (!infoContainer.originalListings && allListings && allListings.length > 0) {
                infoContainer.originalListings = [...allListings];
            }
            if ((!allListings || allListings.length === 0) && infoContainer.originalListings) {
                allListings = [...infoContainer.originalListings];
            }
            const filteredListings = minQty > 0 ? allListings.filter(listing => listing.quantity >= minQty) : allListings;
            if (filteredListings.length === 0 && allListings.length > 0) {
                cardContainer.innerHTML = '';
                const messageContainer = document.createElement('div');
                messageContainer.style.cssText = 'display:flex; flex-direction:column; align-items:center; justify-content:center; padding:20px; text-align:center; width:100%; height:70px;';
                const iconSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
                iconSvg.setAttribute("viewBox", "0 0 512 512");
                iconSvg.setAttribute("width", "24");
                iconSvg.setAttribute("height", "24");
                iconSvg.style.marginBottom = "10px";
                iconSvg.innerHTML = svgTemplates.infoIcon;
                const textDiv = document.createElement('div');
                textDiv.textContent = `No listings found with quantity ≥ ${minQty}. Try a lower value.`;
                messageContainer.appendChild(iconSvg);
                messageContainer.appendChild(textDiv);
                cardContainer.appendChild(messageContainer);
                const countElement = infoContainer.querySelector('.bazaar-listings-count');
                if (countElement) {
                    countElement.textContent = `No listings match minimum quantity of ${minQty} (from ${allListings.length} total listings)`;
                }
                return;
            }

            if (scriptSettings.layoutMode === 'compact') {
                infoContainer.classList.add('compact-layout');
            } else {
                infoContainer.classList.remove('compact-layout');
            }

            if (scriptSettings.layoutMode === 'compact') {
                cardContainer.innerHTML = '';
                cardContainer.style.width = '';

                const fragment = document.createDocumentFragment();
                filteredListings.forEach((listing, index) => {
                    fragment.appendChild(createListingCard(listing, index));
                });
                cardContainer.appendChild(fragment);

                const totalQuantity = filteredListings.reduce((sum, listing) => sum + listing.quantity, 0);
                const countElement = infoContainer.querySelector('.bazaar-listings-count');
                if (countElement) {
                    if (minQty > 0 && filteredListings.length < allListings.length) {
                        countElement.textContent = `Showing ${filteredListings.length} of ${allListings.length} bazaars (${totalQuantity.toLocaleString()} items, min qty: ${minQty})`;
                    } else {
                        countElement.textContent = `Showing ${filteredListings.length} bazaars (${totalQuantity.toLocaleString()} items total)`;
                    }
                }
            } else {
                infoContainer.lastRenderScrollLeft = undefined;
                if (cardContainer.style.width !== (filteredListings.length * CARD_WIDTH) + "px") {
                    cardContainer.style.width = (filteredListings.length * CARD_WIDTH) + "px";
                }

                const scrollLeft = scrollWrapper.scrollLeft,
                      containerWidth = scrollWrapper.clientWidth;
                const visibleCards = Math.ceil(containerWidth / CARD_WIDTH),
                      buffer = Math.max(2, Math.floor(visibleCards / 3));
                const totalItems = filteredListings.length;

                if (infoContainer.lastRenderScrollLeft !== undefined &&
                    Math.abs(infoContainer.lastRenderScrollLeft - scrollLeft) < CARD_WIDTH * 0.3) {

                }
                infoContainer.lastRenderScrollLeft = scrollLeft;

                let startIndex = Math.max(0, Math.floor(scrollLeft / CARD_WIDTH) - buffer),
                    endIndex = Math.min(totalItems, Math.ceil((scrollLeft + containerWidth) / CARD_WIDTH) + buffer);

                const newVisible = {};
                for (let i = startIndex; i < endIndex; i++) {
                    const listing = filteredListings[i];
                    const key = listing.player_id + '-' + listing.price + '-' + listing.quantity;
                    newVisible[key] = i;
                }
                Array.from(cardContainer.children).forEach(card => {
                    if (!card.classList.contains('bazaar-listing-card')) return;
                    const key = card.dataset.listingKey;
                    if (key in newVisible) {
                        const newIndex = newVisible[key];
                        card.dataset.index = newIndex;
                        card.style.left = (newIndex * CARD_WIDTH) + "px";
                        delete newVisible[key];
                    } else {
                        card.classList.add('fade-out');
                        card.addEventListener('transitionend', () => card.remove(), { once: true });
                    }
                });
                const fragment = document.createDocumentFragment();
                for (const key in newVisible) {
                    const newIndex = newVisible[key];
                    const listing = filteredListings[newIndex];
                    const newCard = createListingCard(listing, newIndex);
                    newCard.classList.add('fade-in');
                    fragment.appendChild(newCard);
                    requestAnimationFrame(() => {
                        newCard.classList.remove('fade-in');
                    });
                }
                if (fragment.childElementCount > 0) {
                    cardContainer.appendChild(fragment);
                }
                const totalQuantity = filteredListings.reduce((sum, listing) => sum + listing.quantity, 0);
                const countElement = infoContainer.querySelector('.bazaar-listings-count');
                if (countElement) {
                    if (minQty > 0 && filteredListings.length < allListings.length) {
                        countElement.textContent = `Showing ${filteredListings.length} of ${allListings.length} bazaars (${totalQuantity.toLocaleString()} items total, min qty: ${minQty})`;
                    } else {
                        countElement.textContent = `Showing bazaars ${startIndex + 1}-${endIndex} of ${totalItems} (${totalQuantity.toLocaleString()} items total)`;
                    }
                }
            }

        } catch (error) {
            console.error("Error rendering virtual cards:", error);
        }
    }

    function createInfoContainer(itemName, itemId) {
        const container = document.createElement('div');
        container.className = 'bazaar-info-container';
        container.dataset.itemid = itemId;
        currentItemName = itemName;
        const header = document.createElement('div');
        header.className = 'bazaar-info-header';
        let marketValueText = "";
        try {
            const stored = getStoredItems();
            const match = Object.values(stored).find(item =>
                                                     item.name && item.name.toLowerCase() === itemName.toLowerCase());
            if (match && match.market_value) {
                marketValueText = `Market Value: $${Number(match.market_value).toLocaleString()}`;
            }
        } catch (e) {
            console.error("Header market value error:", e);
        }
        header.textContent = `Bazaar Listings for ${itemName} (ID: ${itemId})`;
        if (marketValueText) {
            const span = document.createElement('span');
            span.style.marginLeft = '8px';
            span.style.fontSize = '14px';
            span.style.fontWeight = 'normal';
            span.style.color = currentDarkMode ? '#aaa' : '#666';
            span.textContent = `• ${marketValueText}`;
            header.appendChild(span);
        }
        container.appendChild(header);
        currentSortOrder = getSortOrderForKey(currentSortKey);
        const sortControls = document.createElement('div');
        sortControls.className = 'bazaar-sort-controls';
        sortControls.innerHTML = `
            <span>Sort by:</span>
            <select class="bazaar-sort-select">
                <option value="price" ${currentSortKey === "price" ? "selected" : ""}>Price</option>
                <option value="quantity" ${currentSortKey === "quantity" ? "selected" : ""}>Quantity</option>
                <option value="profit" ${currentSortKey === "profit" ? "selected" : ""}>Profit</option>
                <option value="updated" ${currentSortKey === "updated" ? "selected" : ""}>Last Updated</option>
            </select>
            <button class="bazaar-button bazaar-order-toggle">
                ${currentSortOrder === "asc" ? "Asc" : "Desc"}
            </button>
            <button class="bazaar-button bazaar-display-toggle" title="Toggle between percentage difference and total profit">
                ${displayMode === "percentage" ? "%" : "$"}
            </button>
            <button class="bazaar-button bazaar-layout-toggle" title="Toggle Card Layout">
                ${scriptSettings.layoutMode === 'compact' ? 'Compact': 'Default'}
            </button>
            <span style="margin-left: 8px;">Min Qty:</span>
            <input type="number" class="bazaar-min-qty" style="width: 60px; padding: 3px; border: 1px solid #ccc; border-radius: 4px;" min="0" placeholder="" value="1">
        `;
        container.appendChild(sortControls);
        const scrollContainer = document.createElement('div');
        scrollContainer.className = 'bazaar-scroll-container';
        function createScrollArrow(direction) {
            const arrow = document.createElement('div');
            arrow.className = `bazaar-scroll-arrow ${direction}`;
            arrow.innerHTML = svgTemplates[direction === 'left' ? 'leftArrow' : 'rightArrow'];
            let isScrolling = false,
                scrollAnimationId = null,
                startTime = 0,
                isClickAction = false;
            const ACTION_THRESHOLD = 200;
            function smoothScroll() {
                if (!isScrolling) return;
                scrollWrapper.scrollLeft += (direction === 'left' ? -1.5 : 1.5);
                scrollAnimationId = requestAnimationFrame(smoothScroll);
            }
            function startScrolling(e) {
                e.preventDefault();
                startTime = Date.now();
                isClickAction = false;
                setTimeout(() => {
                    if (startTime && Date.now() - startTime >= ACTION_THRESHOLD) {
                        isScrolling = true;
                        smoothScroll();
                    }
                }, ACTION_THRESHOLD);
            }
            function stopScrolling() {
                const holdDuration = Date.now() - startTime;
                isScrolling = false;
                if (scrollAnimationId) {
                    cancelAnimationFrame(scrollAnimationId);
                    scrollAnimationId = null;
                }
                if (holdDuration < ACTION_THRESHOLD && !isClickAction) {
                    isClickAction = true;
                    scrollWrapper.scrollBy({ left: direction === 'left' ? -200 : 200, behavior: 'smooth' });
                }
                startTime = 0;
            }
            arrow.addEventListener('mousedown', startScrolling);
            arrow.addEventListener('mouseup', stopScrolling);
            arrow.addEventListener('mouseleave', stopScrolling);
            arrow.addEventListener('touchstart', startScrolling, { passive: false });
            arrow.addEventListener('touchend', stopScrolling);
            arrow.addEventListener('touchcancel', stopScrolling);
            return arrow;
        }
        scrollContainer.appendChild(createScrollArrow('left'));
        const scrollWrapper = document.createElement('div');
        scrollWrapper.className = 'bazaar-scroll-wrapper';
        const cardContainer = document.createElement('div');
        cardContainer.className = 'bazaar-card-container';
        scrollWrapper.appendChild(cardContainer);
        scrollContainer.appendChild(scrollWrapper);
        scrollContainer.appendChild(createScrollArrow('right'));
        scrollWrapper.addEventListener('scroll', () => {
            if (!scrollWrapper.isScrolling) {
                scrollWrapper.isScrolling = true;
                requestAnimationFrame(function checkScroll() {
                    renderVirtualCards(container);
                    if (scrollWrapper.lastKnownScrollLeft === scrollWrapper.scrollLeft) {
                        renderVirtualCards(container);
                        scrollWrapper.isScrolling = false;
                    } else {
                        scrollWrapper.lastKnownScrollLeft = scrollWrapper.scrollLeft;
                        requestAnimationFrame(checkScroll);
                    }
                });
            }
        });
        container.appendChild(scrollContainer);
        const footerContainer = document.createElement('div');
        footerContainer.className = 'bazaar-footer-container';
        const listingsCount = document.createElement('div');
        listingsCount.className = 'bazaar-listings-count';
        listingsCount.textContent = 'Loading...';
        footerContainer.appendChild(listingsCount);

        // Add registration link to footer
        const registrationLink = document.createElement('div');
        registrationLink.style.cssText = 'font-size:11px; color:#666; margin-left:auto;';
        registrationLink.innerHTML = `<a href="https://weav3r.dev/" target="_blank" style="color:inherit; text-decoration:underline;">Register your API key to show your own listings here!</a>`;
        if (currentDarkMode) {
            registrationLink.style.color = '#aaa';
        }
        footerContainer.appendChild(registrationLink);
        container.appendChild(footerContainer);
        return container;
    }

    function sortListings(listings) {
        return listings.slice().sort((a, b) => {
            let diff;
            if (currentSortKey === "profit") {
                try {
                    const stored = getStoredItems();
                    const match = Object.values(stored).find(item =>
                                                             item.name && item.name.toLowerCase() === currentItemName.toLowerCase());
                    if (match && match.market_value) {
                        const marketValue = Number(match.market_value),
                              fee = scriptSettings.listingFee || 0,
                              aProfit = (marketValue * a.quantity) - (a.price * a.quantity) - Math.ceil((marketValue * a.quantity) * (fee / 100)),
                              bProfit = (marketValue * b.quantity) - (b.price * b.quantity) - Math.ceil((marketValue * b.quantity) * (fee / 100));
                        diff = aProfit - bProfit;
                    } else {
                        diff = a.price - b.price;
                    }
                } catch (e) {
                    console.error("Profit sort error:", e);
                    diff = a.price - b.price;
                }
            } else {
                diff = currentSortKey === "price" ? a.price - b.price :
                currentSortKey === "quantity" ? a.quantity - b.quantity :
                a.updated - b.updated;
            }
            return currentSortOrder === "asc" ? diff : -diff;
        });
    }

    function updateInfoContainer(wrapper, itemId, itemName) {
        if (wrapper.hasAttribute('data-has-bazaar-info')) return;
        let infoContainer = document.querySelector(`.bazaar-info-container[data-itemid="${itemId}"]`);
        if (!infoContainer) {
            infoContainer = createInfoContainer(itemName, itemId);
            wrapper.insertBefore(infoContainer, wrapper.firstChild);
            wrapper.setAttribute('data-has-bazaar-info', 'true');
        } else if (!wrapper.contains(infoContainer)) {
            infoContainer = createInfoContainer(itemName, itemId);
            wrapper.insertBefore(infoContainer, wrapper.firstChild);
            wrapper.setAttribute('data-has-bazaar-info', 'true');
        } else {
            const header = infoContainer.querySelector('.bazaar-info-header');
            if (header) {
                header.textContent = `Bazaar Listings for ${itemName} (ID: ${itemId})`;
            }
        }
        const cardContainer = infoContainer.querySelector('.bazaar-card-container');
        const countElement = infoContainer.querySelector('.bazaar-listings-count');
        const updateListingsCount = (text) => {
            if (countElement) {
                countElement.textContent = text;
            }
        };
        const showEmptyState = (isError) => {
            if (cardContainer) {
                cardContainer.innerHTML = '';
                cardContainer.style.width = '';
                renderMessageInContainer(cardContainer, isError);
            }
            updateListingsCount(isError ? 'API Error - Check back later' : 'No listings available');
        };
        if (cardContainer) {
            cardContainer.innerHTML = '<div style="padding:10px; text-align:center; width:100%;">Loading bazaar listings...</div>';
        }
        const cachedData = getCache(itemId);
        if (cachedData) {
            allListings = sortListings(cachedData.listings);
            if (allListings.length === 0) {
                showEmptyState(false);
            } else {
                renderVirtualCards(infoContainer);
            }
            return;
        }
        let listings = [], apiErrors = false;
        let requestTimeout = setTimeout(() => {
            console.warn('Bazaar listings request timed out');
            showEmptyState(true);
        }, 15000);

        fetchJSON(`https://weav3r.dev/api/marketplace/${itemId}`, data => {
            clearTimeout(requestTimeout);
            if (!data || !data.listings) {
                showEmptyState(true);
                return;
            }
            listings = data.listings.map(listing => ({
                item_id: listing.item_id,
                player_id: listing.player_id,
                player_name: listing.player_name,
                quantity: listing.quantity,
                price: listing.price,
                updated: listing.last_checked
            }));
            setCache(itemId, { listings });
            if (listings.length === 0) {
                showEmptyState(false);
            } else {
                allListings = sortListings(listings);
                renderVirtualCards(infoContainer);
            }
        });
    }

    function renderMessageInContainer(container, isApiError) {
        container.innerHTML = '';
        const messageContainer = document.createElement('div');
        messageContainer.style.cssText = 'display:flex; flex-direction:column; align-items:center; justify-content:center; padding:20px; text-align:center; width:100%; height:70px;';
        const iconSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        iconSvg.setAttribute("viewBox", "0 0 512 512");
        iconSvg.setAttribute("width", "24");
        iconSvg.setAttribute("height", "24");
        iconSvg.style.marginBottom = "10px";
        const textDiv = document.createElement('div');
        if (isApiError) {
            iconSvg.innerHTML = svgTemplates.infoIcon;
            textDiv.innerHTML = "API Error<br><span style='font-size: 12px; color: #666;'>Please try again later</span>";
            textDiv.style.cssText = currentDarkMode ? 'color:#aaa;' : 'color:#333;';
        } else {
            iconSvg.innerHTML = svgTemplates.infoIcon;
            textDiv.textContent = "No bazaar listings available for this item.";
        }
        messageContainer.appendChild(iconSvg);
        messageContainer.appendChild(textDiv);
        container.appendChild(messageContainer);
    }

    function processSellerWrapper(wrapper) {
        if (!wrapper || wrapper.classList.contains('bazaar-info-container') || wrapper.hasAttribute('data-bazaar-processed')) return;
        const existingContainer = wrapper.querySelector(':scope > .bazaar-info-container');
        if (existingContainer) return;
        const itemTile = wrapper.previousElementSibling;
        if (!itemTile) return;
        const nameEl = itemTile.querySelector('.name___ukdHN'),
              btn = itemTile.querySelector('button[aria-controls^="wai-itemInfo-"]');
        if (nameEl && btn) {
            const itemName = nameEl.textContent.trim();
            const idParts = btn.getAttribute('aria-controls').split('-');
            const itemId = idParts[idParts.length - 1];
            wrapper.setAttribute('data-bazaar-processed', 'true');
            updateInfoContainer(wrapper, itemId, itemName);
        }
    }

    function processMobileSellerList() {
        if (!checkMobileView()) return;
        const sellerList = document.querySelector('ul.sellerList___e4C9_, ul[class*="sellerList"]');
        if (!sellerList) {
            const existing = document.querySelector('.bazaar-info-container');
            if (existing && !document.contains(existing.parentNode)) {
                existing.remove();
            }
            return;
        }
        if (sellerList.hasAttribute('data-has-bazaar-container')) {
            return;
        }
        const headerEl = document.querySelector('.itemsHeader___ZTO9r .title___ruNCT, [class*="itemsHeader"] [class*="title"]');
        const itemName = headerEl ? headerEl.textContent.trim() : "Unknown";
        const btn = document.querySelector('.itemsHeader___ZTO9r button[aria-controls^="wai-itemInfo-"], [class*="itemsHeader"] button[aria-controls^="wai-itemInfo-"]');
        let itemId = "unknown";
        if (btn) {
            const parts = btn.getAttribute('aria-controls').split('-');
            itemId = parts.length > 2 ? parts[parts.length - 2] : parts[parts.length - 1];
        }
        const existingContainer = document.querySelector(`.bazaar-info-container[data-itemid="${itemId}"]`);
        if (existingContainer) {
            if (existingContainer.parentNode !== sellerList.parentNode ||
                existingContainer.nextSibling !== sellerList) {
                sellerList.parentNode.insertBefore(existingContainer, sellerList);
            }
            return;
        }
        const infoContainer = createInfoContainer(itemName, itemId);
        sellerList.parentNode.insertBefore(infoContainer, sellerList);
        sellerList.setAttribute('data-has-bazaar-container', 'true');
        updateInfoContainer(infoContainer, itemId, itemName);
    }

    function processAllSellerWrappers(root = document.body) {
        if (checkMobileView()) return;
        const sellerWrappers = root.querySelectorAll('[class*="sellerListWrapper"]');
        sellerWrappers.forEach(wrapper => processSellerWrapper(wrapper));
    }
    processAllSellerWrappers();
    processMobileSellerList();

    const observeTarget = document.querySelector('#root') || document.body;
    let isProcessing = false;
    const observer = new MutationObserver(mutations => {
        if (isProcessing) return;
        let needsProcessing = false;
        mutations.forEach(mutation => {
            const isOurMutation = Array.from(mutation.addedNodes).some(node =>
                                                                       node.nodeType === Node.ELEMENT_NODE &&
                                                                       (node.classList.contains('bazaar-info-container') ||
                                                                        node.querySelector('.bazaar-info-container'))
                                                                      );
            if (isOurMutation) return;
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    needsProcessing = true;
                }
            });
            mutation.removedNodes.forEach(node => {
                if (node.nodeType === Node.ELEMENT_NODE &&
                    (node.matches('ul.sellerList___e4C9_') || node.matches('ul[class*="sellerList"]')) &&
                    checkMobileView()) {
                    const container = document.querySelector('.bazaar-info-container');
                    if (container) container.remove();
                }
            });
        });
        if (needsProcessing) {
            if (observer.processingTimeout) {
                clearTimeout(observer.processingTimeout);
            }
            observer.processingTimeout = setTimeout(() => {
                try {
                    isProcessing = true;
                    if (checkMobileView()) {
                        processMobileSellerList();
                    } else {
                        processAllSellerWrappers();
                    }
                } finally {
                    isProcessing = false;
                    observer.processingTimeout = null;
                }
            }, 100);
        }
    });
    observer.observe(observeTarget, { childList: true, subtree: true });
    const bodyObserver = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            if (mutation.attributeName === 'class') {
                currentDarkMode = document.body.classList.contains('dark-mode');
            }
        });
    });
    bodyObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });

    if (window.location.href.includes("bazaar.php")) {
        function scrollToTargetItem() {
            const params = new URLSearchParams(window.location.search);
            const targetItemId = params.get("itemId"), highlight = params.get("highlight");
            if (!targetItemId || highlight !== "1") return;
            function removeHighlightParam() {
                params.delete("highlight");
                history.replaceState({}, "", window.location.pathname + "?" + params.toString() + window.location.hash);
            }
            function showToast(message) {
                const toast = document.createElement('div');
                toast.textContent = message;
                toast.style.cssText = 'position:fixed; bottom:20px; left:50%; transform:translateX(-50%); background-color:rgba(0,0,0,0.7); color:white; padding:10px 20px; border-radius:5px; z-index:100000; font-size:14px;';
                document.body.appendChild(toast);
                setTimeout(() => {
                    toast.remove();
                }, 3000);
            }
            function findItemCard() {
                const img = document.querySelector(`img[src*="/images/items/${targetItemId}/"]`);
                return img ? img.closest('.item___GYCYJ') : null;
            }
            const scrollInterval = setInterval(() => {
                const card = findItemCard();
                if (card) {
                    clearInterval(scrollInterval);
                    removeHighlightParam();
                    card.classList.add("green-outline", "pop-flash");
                    card.scrollIntoView({ behavior: "smooth", block: "center" });
                    setTimeout(() => {
                        card.classList.remove("pop-flash");
                    }, 800);
                } else {
                    if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
                        showToast("Item not found on this page.");
                        removeHighlightParam();
                        clearInterval(scrollInterval);
                    } else {
                        window.scrollBy({ top: 300, behavior: 'auto' });
                    }
                }
            }, 50);
        }
        function waitForItems() {
            const container = document.querySelector('[data-testid="bazaar-items"]');
            if (container && container.childElementCount > 0) {
                scrollToTargetItem();
            } else {
                setTimeout(waitForItems, 500);
            }
        }
        waitForItems();
    }

    function dailyCleanup() {
        const lastCleanup = GM_getValue("lastDailyCleanup"),
              oneDay = 24 * 60 * 60 * 1000,
              now = Date.now();
        if (!lastCleanup || (now - parseInt(lastCleanup, 10)) > oneDay) {
            const sevenDays = 7 * 24 * 60 * 60 * 1000;

            let keys = [];
            try {
                if (typeof GM_listValues === 'function') {
                    keys = GM_listValues();
                }
                if (keys.length === 0) {
                    const checkKey = (prefix) => {
                        let i = 0;
                        while (true) {
                            const testKey = `${prefix}${i}`;
                            const value = GM_getValue(testKey);
                            if (value === undefined) break;
                            keys.push(testKey);
                            i++;
                        }
                    };

                    ['visited_', 'tornBazaarCache_'].forEach(prefix => {
                        for (let id = 1; id <= 1000; id++) {
                            const key = `${prefix}${id}`;
                            const value = GM_getValue(key);
                            if (value !== undefined) {
                                keys.push(key);
                            }
                        }
                    });
                }
            } catch (e) {
                console.error("Error listing storage keys:", e);
            }

            keys.forEach(key => {
                if (key && (key.startsWith("visited_") || key.startsWith("tornBazaarCache_"))) {
                    try {
                        const val = JSON.parse(GM_getValue(key));
                        let ts = null;
                        if (key.startsWith("visited_") && val && val.lastClickedUpdated) {
                            ts = val.lastClickedUpdated;
                        } else if (key.startsWith("tornBazaarCache_") && val && val.timestamp) {
                            ts = val.timestamp;
                        } else {
                            GM_deleteValue(key);
                        }
                        if (ts !== null && (now - ts) > sevenDays) {
                            GM_deleteValue(key);
                        }
                    } catch (e) {
                        GM_deleteValue(key);
                    }
                }
            });

            GM_setValue("lastDailyCleanup", now.toString());
        }
    }
    dailyCleanup();

    document.body.addEventListener('click', event => {
        const container = event.target.closest('.bazaar-info-container');
        if (!container) return;
        if (event.target.matches('.bazaar-order-toggle')) {
            currentSortOrder = currentSortOrder === "asc" ? "desc" : "asc";
            event.target.textContent = currentSortOrder === "asc" ? "Asc" : "Desc";
            performSort(container);
        }
        if (event.target.matches('.bazaar-display-toggle')) {
            displayMode = displayMode === "percentage" ? "profit" : "percentage";
            event.target.textContent = displayMode === "percentage" ? "%" : "$";
            scriptSettings.defaultDisplayMode = displayMode;
            saveSettings();

            const allContainers = document.querySelectorAll('.bazaar-info-container');
            allContainers.forEach(container => {
                renderVirtualCards(container);

                const cardContainer = container.querySelector('.bazaar-card-container');
                if (cardContainer) {
                    const scrollWrapper = container.querySelector('.bazaar-scroll-wrapper');
                    const currentScroll = scrollWrapper ? scrollWrapper.scrollLeft : 0;

                    const itemId = container.dataset.itemid;
                    if (itemId) {
                        if (allListings && allListings.length > 0) {
                            cardContainer.innerHTML = '';
                            renderVirtualCards(container);

                            if (scrollWrapper) {
                                scrollWrapper.scrollLeft = currentScroll;
                            }
                        }
                    }
                }
            });

            return;
        }
        if (event.target.matches('.bazaar-layout-toggle')) {
            const button = event.target;
            scriptSettings.layoutMode = scriptSettings.layoutMode === 'default' ? 'compact' : 'default';
            button.textContent = scriptSettings.layoutMode === 'compact' ? 'Compact' : 'Default';
            saveSettings();
            performSort(container); // is this the right func?
        }
    });

    document.body.addEventListener('input', event => {
        const container = event.target.closest('.bazaar-info-container');
        if (!container) return;
        if (event.target.matches('.bazaar-min-qty')) {
            clearTimeout(event.target.debounceTimer);
            event.target.debounceTimer = setTimeout(() => {
                const scrollWrapper = container.querySelector('.bazaar-scroll-wrapper');
                if (scrollWrapper) {
                    scrollWrapper.scrollLeft = 0;
                }
                container.lastRenderScrollLeft = undefined;
                if (!allListings || allListings.length === 0) {
                    const itemId = container.getAttribute('data-itemid');
                    if (itemId) {
                        const cachedData = getCache(itemId);
                        if (cachedData && cachedData.listings && cachedData.listings.length > 0) {
                            allListings = sortListings(cachedData.listings);
                        }
                    }
                }
                renderVirtualCards(container);
            }, 300);
        }
    });

    document.body.addEventListener('change', event => {
        const container = event.target.closest('.bazaar-info-container');
        if (!container) return;
        if (event.target.matches('.bazaar-sort-select')) {
            const newSortKey = event.target.value;
            if (newSortKey !== currentSortKey) {
                currentSortKey = newSortKey;
                currentSortOrder = getSortOrderForKey(currentSortKey);
                const orderToggle = container.querySelector('.bazaar-order-toggle');
                if (orderToggle) {
                    orderToggle.textContent = currentSortOrder === "asc" ? "Asc" : "Desc";
                }
            } else {
                currentSortKey = newSortKey;
            }
            performSort(container);
        }
    });

    function performSort(container) {
        allListings = sortListings(allListings);
        const cardContainer = container.querySelector('.bazaar-card-container');
        const scrollWrapper = container.querySelector('.bazaar-scroll-wrapper');
        if (cardContainer && scrollWrapper) {
            scrollWrapper.scrollLeft = 0;
            container.lastRenderScrollLeft = undefined;
            renderVirtualCards(container);
        }
    }

    function addSettingsMenuItem() {
        const menu = document.querySelector('.settings-menu');
        if (!menu || document.querySelector('.bazaar-settings-button')) return;
        const li = document.createElement('li');
        li.className = 'link bazaar-settings-button';
        const a = document.createElement('a');
        a.href = '#';
        const iconDiv = document.createElement('div');
        iconDiv.className = 'icon-wrapper';
        const svgIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svgIcon.setAttribute('class', 'default');
        svgIcon.setAttribute('fill', '#fff');
        svgIcon.setAttribute('stroke', 'transparent');
        svgIcon.setAttribute('stroke-width', '0');
        svgIcon.setAttribute('width', '16');
        svgIcon.setAttribute('height', '16');
        svgIcon.setAttribute('viewBox', '0 0 640 512');
        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path.setAttribute('d', 'M36.8 192l566.3 0c20.3 0 36.8-16.5 36.8-36.8c0-7.3-2.2-14.4-6.2-20.4L558.2 21.4C549.3 8 534.4 0 518.3 0L121.7 0c-16 0-31 8-39.9 21.4L6.2 134.7c-4 6.1-6.2 13.2-6.2 20.4C0 175.5 16.5 192 36.8 192zM64 224l0 160 0 80c0 26.5 21.5 48 48 48l224 0c26.5 0 48-21.5 48-48l0-80 0-160-64 0 0 160-192 0 0-160-64 0zm448 0l0 256c0 17.7 14.3 32 32 32s32-14.3 32-32l0-256-64 0z');
        const span = document.createElement('span');
        span.textContent = 'Bazaar Settings';
        svgIcon.appendChild(path);
        iconDiv.appendChild(svgIcon);
        a.appendChild(iconDiv);
        a.appendChild(span);
        li.appendChild(a);
        a.addEventListener('click', e => {
            e.preventDefault();
            document.body.click();
            openSettingsModal();
        });
        const logoutButton = menu.querySelector('li.logout');
        if (logoutButton) {
            menu.insertBefore(li, logoutButton);
        } else {
            menu.appendChild(li);
        }
    }

    function openSettingsModal() {
        const overlay = document.createElement("div");
        overlay.className = "bazaar-modal-overlay";
        const modal = document.createElement("div");
        modal.className = "bazaar-settings-modal";
        modal.innerHTML = `
            <div class="bazaar-settings-title">Bazaar Listings Settings</div>
            <div class="bazaar-tabs">
                <div class="bazaar-tab active" data-tab="settings">Settings</div>
                <div class="bazaar-tab" data-tab="scripts">Other Scripts</div>
            </div>
            <div class="bazaar-tab-content active" id="tab-settings" style="max-height: 350px; overflow-y: auto;">
                <div class="bazaar-settings-group">
                    <div class="bazaar-settings-item">
                        <label for="bazaar-api-key">Torn API Key (Optional)</label>
                        <div style="display: flex; gap: 5px; align-items: center; width: 100%;">
                            <input type="text" id="bazaar-api-key" value="${scriptSettings.apiKey || ''}" placeholder="Enter your API key here" style="flex-grow: 1; max-width: none;">
                            <button class="bazaar-button refresh-market-data" id="refresh-market-data" style="white-space: nowrap; padding: 8px 10px; height: 35px;">Refresh Values</button>
                        </div>
                        <div id="refresh-status" style="margin-top: 5px; font-size: 12px; display: none;"></div>
                        <div class="bazaar-api-note">
                            Providing an API key enables market value comparison. Your key stays local.
                        </div>
                    </div>
                    <div class="bazaar-settings-item">
                        <label for="bazaar-default-sort">Default Sort</label>
                        <select id="bazaar-default-sort">
                            <option value="price" ${scriptSettings.defaultSort === 'price' ? 'selected' : ''}>Price</option>
                            <option value="quantity" ${scriptSettings.defaultSort === 'quantity' ? 'selected' : ''}>Quantity</option>
                            <option value="profit" ${scriptSettings.defaultSort === 'profit' ? 'selected' : ''}>Profit</option>
                            <option value="updated" ${scriptSettings.defaultSort === 'updated' ? 'selected' : ''}>Last Updated</option>
                        </select>
                        <div class="bazaar-api-note">
                            Choose how listings are sorted: Price, Quantity, Profit, or Last Updated.
                        </div>
                    </div>
                    <div class="bazaar-settings-item">
                        <label for="bazaar-default-order">Default Order</label>
                        <select id="bazaar-default-order">
                            <option value="asc" ${scriptSettings.defaultOrder === 'asc' ? 'selected' : ''}>Ascending</option>
                            <option value="desc" ${scriptSettings.defaultOrder === 'desc' ? 'selected' : ''}>Descending</option>
                        </select>
                        <div class="bazaar-api-note">
                            Choose the sorting direction.
                        </div>
                    </div>
                    <div class="bazaar-settings-item">
                        <label for="bazaar-listing-fee">Listing Fee (%)</label>
                        <input type="number" id="bazaar-listing-fee" class="bazaar-number-input" value="${scriptSettings.listingFee || 0}" min="0" max="100" step="1">
                        <div class="bazaar-api-note">
                            Set the fee percentage when listing items. (e.g., 10% fee means $10,000 on $100,000)
                        </div>
                    </div>
                    <div class="bazaar-settings-item">
                        <label for="bazaar-default-display">Default Display Mode</label>
                        <select id="bazaar-default-display">
                            <option value="percentage" ${scriptSettings.defaultDisplayMode === 'percentage' ? 'selected' : ''}>Percentage Difference</option>
                            <option value="profit" ${scriptSettings.defaultDisplayMode === 'profit' ? 'selected' : ''}>Potential Profit</option>
                        </select>
                        <div class="bazaar-api-note">
                            Choose whether to display price comparisons as a percentage or in dollars.
                        </div>
                    </div>
                    <div class="bazaar-settings-item">
                        <label for="bazaar-link-behavior">Bazaar Link Click Behavior</label>
                        <select id="bazaar-link-behavior">
                            <option value="new_tab" ${scriptSettings.linkBehavior === 'new_tab' ? 'selected' : ''}>Open in New Tab</option>
                            <option value="new_window" ${scriptSettings.linkBehavior === 'new_window' ? 'selected' : ''}>Open in New Window</option>
                            <option value="same_tab" ${scriptSettings.linkBehavior === 'same_tab' ? 'selected' : ''}>Open in Same Tab</option>
                        </select>
                        <div class="bazaar-api-note">
                            Choose how bazaar links open when clicked.
                        </div>
                    </div>
                    <div class="bazaar-settings-item">
                        <label for="bazaar-layout-mode">Card Layout</label>
                        <select id="bazaar-layout-mode">
                            <option value="default" ${scriptSettings.layoutMode === 'default' ? 'selected' : ''}>Default (Horizontal Scroll)</option>
                            <option value="compact" ${scriptSettings.layoutMode === 'compact' ? 'selected' : ''}>Compact (Vertical List)</option>
                        </select>
                        <div class="bazaar-api-note">
                            Choose the display style for bazaar listings.
                        </div>
                    </div>
                </div>
            </div>
            <div class="bazaar-tab-content" id="tab-scripts" style="max-height: 350px; overflow-y: auto;">
                <div class="bazaar-script-item">
                    <div class="bazaar-script-name">Customizable Bazaar Filler</div>
                    <div class="bazaar-script-desc">Auto-fills bazaar item quantities and prices.</div>
                    <a href="https://gf.qytechs.cn/en/scripts/527925-customizable-bazaar-filler" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
                </div>
                <div class="bazaar-script-item">
                    <div class="bazaar-script-name">Torn Item Market Highlighter</div>
                    <div class="bazaar-script-desc">Highlights items based on rules and prices.</div>
                    <a href="https://gf.qytechs.cn/en/scripts/513617-torn-item-market-highlighter" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
                </div>
                <div class="bazaar-script-item">
                    <div class="bazaar-script-name">Torn Item Market Max Quantity Calculator</div>
                    <div class="bazaar-script-desc">Calculates the max quantity you can buy.</div>
                    <a href="https://gf.qytechs.cn/en/scripts/513790-torn-item-market-max-quantity-calculator" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
                </div>
                <div class="bazaar-script-item">
                    <div class="bazaar-script-name">Enhanced Chat Buttons V2</div>
                    <div class="bazaar-script-desc">Improves chat with extra buttons.</div>
                    <a href="https://gf.qytechs.cn/en/scripts/488294-torn-com-enhanced-chat-buttons-v2" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
                </div>
                <div class="bazaar-script-item">
                    <div class="bazaar-script-name">Market Item Locker</div>
                    <div class="bazaar-script-desc">Lock items when listing to avoid accidental sales.</div>
                    <a href="https://gf.qytechs.cn/en/scripts/513784-torn-market-item-locker" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
                </div>
                <div class="bazaar-script-item">
                    <div class="bazaar-script-name">Market Quick Remove</div>
                    <div class="bazaar-script-desc">Quickly remove items from your listings.</div>
                    <a href="https://gf.qytechs.cn/en/scripts/515870-torn-market-quick-remove" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
                </div>
                <div class="bazaar-script-item">
                    <div class="bazaar-script-name">Trade Chat Timer on Button</div>
                    <div class="bazaar-script-desc">Adds a timer to the trade chat button.</div>
                    <a href="https://gf.qytechs.cn/en/scripts/496284-trade-chat-timer-on-button" target="_blank" class="bazaar-script-link">Install from Greasy Fork镜像</a>
                </div>
            </div>
            <div class="bazaar-settings-buttons">
                <button class="bazaar-settings-save">Save</button>
                <button class="bazaar-settings-cancel">Cancel</button>
            </div>
            <div class="bazaar-settings-footer">
                <p>Created by <a href="https://www.torn.com/profiles.php?XID=1853324" target="_blank">Weav3r [1853324]</a> • Powered by <a href="https://weav3r.dev/" target="_blank">weav3r.dev</a></p>
            </div>
        `;
        overlay.appendChild(modal);
        const tabs = modal.querySelectorAll('.bazaar-tab');
        tabs.forEach(tab => {
            tab.addEventListener('click', function () {
                tabs.forEach(t => t.classList.remove('active'));
                this.classList.add('active');
                modal.querySelectorAll('.bazaar-tab-content').forEach(content => content.classList.remove('active'));
                document.getElementById(`tab-${this.getAttribute('data-tab')}`).classList.add('active');
            });
        });
        modal.querySelector('.bazaar-settings-save').addEventListener('click', () => {
            saveSettingsFromModal(modal);
            overlay.remove();
        });
        modal.querySelector('.bazaar-settings-cancel').addEventListener('click', () => {
            overlay.remove();
        });
        overlay.addEventListener('click', e => {
            if (e.target === overlay) overlay.remove();
        });
        document.body.appendChild(overlay);
    }

    function saveSettingsFromModal(modal) {
        const oldLinkBehavior = scriptSettings.linkBehavior;
        scriptSettings.apiKey = modal.querySelector('#bazaar-api-key').value.trim();
        scriptSettings.defaultSort = modal.querySelector('#bazaar-default-sort').value;
        scriptSettings.defaultOrder = modal.querySelector('#bazaar-default-order').value;
        scriptSettings.listingFee = Math.round(parseFloat(modal.querySelector('#bazaar-listing-fee').value) || 0);
        scriptSettings.defaultDisplayMode = modal.querySelector('#bazaar-default-display').value;
        scriptSettings.linkBehavior = modal.querySelector('#bazaar-link-behavior').value;
        scriptSettings.layoutMode = modal.querySelector('#bazaar-layout-mode').value;

        if (scriptSettings.listingFee < 0) scriptSettings.listingFee = 0;
        if (scriptSettings.listingFee > 100) scriptSettings.listingFee = 100;
        currentSortKey = scriptSettings.defaultSort;
        currentSortOrder = scriptSettings.defaultOrder;
        displayMode = scriptSettings.defaultDisplayMode;
        saveSettings();
        document.querySelectorAll('.bazaar-info-container').forEach(container => {
            const sortSelect = container.querySelector('.bazaar-sort-select');
            if (sortSelect) sortSelect.value = currentSortKey;
            const orderToggle = container.querySelector('.bazaar-order-toggle');
            if (orderToggle) orderToggle.textContent = currentSortOrder === "asc" ? "Asc" : "Desc";
            const displayToggle = container.querySelector('.bazaar-display-toggle');
            if (displayToggle) displayToggle.textContent = displayMode === "percentage" ? "%" : "$";
            if (oldLinkBehavior !== scriptSettings.linkBehavior) {
                const cardContainer = container.querySelector('.bazaar-card-container');
                if (cardContainer) {
                    cardContainer.innerHTML = '';
                    container.lastRenderScrollLeft = undefined;
                    renderVirtualCards(container);
                }
            } else {
                performSort(container);
            }
        });
        if (scriptSettings.apiKey) {
            fetchTornItems(true);
        }
    }

    function fetchTornItems(forceRefresh = false) {
        const stored = GM_getValue("tornItems"),
              lastUpdated = GM_getValue("lastTornItemsUpdate") || 0,
              now = Date.now(),
              oneDayMs = 24 * 60 * 60 * 1000,
              lastUTC = new Date(parseInt(lastUpdated)).toISOString().split('T')[0],
              todayUTC = new Date().toISOString().split('T')[0],
              lastHour = Math.floor(parseInt(lastUpdated) / (60 * 60 * 1000)),
              currentHour = Math.floor(now / (60 * 60 * 1000));

        const needsRefresh = forceRefresh ||
              lastUTC < todayUTC ||
              (now - lastUpdated) >= oneDayMs ||
              (lastHour < currentHour && (currentHour - lastHour) >= 1);

        if (scriptSettings.apiKey && (!stored || needsRefresh)) {
            const refreshStatus = document.getElementById('refresh-status');
            if (refreshStatus) {
                refreshStatus.style.display = 'block';
                refreshStatus.textContent = 'Fetching market values...';
                refreshStatus.style.color = currentDarkMode ? '#aaa' : '#666';
            }

            return fetch(`https://api.torn.com/torn/?key=${scriptSettings.apiKey}&selections=items&comment=wBazaars`)
                .then(r => r.json())
                .then(data => {
                if (!data.items) {
                    console.error("Failed to fetch Torn items. Check your API key or rate limit.");
                    if (refreshStatus) {
                        refreshStatus.textContent = data.error ? `Error: ${data.error.error}` : 'Failed to fetch market values. Check your API key.';
                        refreshStatus.style.color = '#cc0000';
                        setTimeout(() => {
                            refreshStatus.style.display = 'none';
                        }, 5000);
                    }
                    return false;
                }

                cachedItemsData = null;

                const filtered = {};
                for (let [id, item] of Object.entries(data.items)) {
                    if (item.tradeable) {
                        filtered[id] = { name: item.name, market_value: item.market_value };
                    }
                }
                GM_setValue("tornItems", JSON.stringify(filtered));
                GM_setValue("lastTornItemsUpdate", now.toString());

                if (refreshStatus) {
                    refreshStatus.textContent = `Market values updated successfully! (${todayUTC})`;
                    refreshStatus.style.color = '#009900';
                    setTimeout(() => {
                        refreshStatus.style.display = 'none';
                    }, 3000);
                }

                document.querySelectorAll('.bazaar-info-container').forEach(container => {
                    if (container.isConnected) {
                        const cardContainer = container.querySelector('.bazaar-card-container');
                        if (cardContainer) {
                            cardContainer.innerHTML = '';
                            container.lastRenderScrollLeft = undefined;
                            renderVirtualCards(container);
                        }
                    }
                });

                return true;
            })
                .catch(err => {
                console.error("Error fetching Torn items:", err);
                if (refreshStatus) {
                    refreshStatus.textContent = `Error: ${err.message || 'Failed to fetch market values'}`;
                    refreshStatus.style.color = '#cc0000';
                    setTimeout(() => {
                        refreshStatus.style.display = 'none';
                    }, 5000);
                }
                return false;
            });
        }
        return Promise.resolve(false);
    }

    document.body.addEventListener('click', event => {
        if (event.target.id === 'refresh-market-data' || event.target.closest('#refresh-market-data')) {
            event.preventDefault();
            const apiKeyInput = document.getElementById('bazaar-api-key');
            const refreshStatus = document.getElementById('refresh-status');

            if (!apiKeyInput || !apiKeyInput.value.trim()) {
                if (refreshStatus) {
                    refreshStatus.style.display = 'block';
                    refreshStatus.textContent = 'Please enter an API key first.';
                    refreshStatus.style.color = '#cc0000';
                    setTimeout(() => {
                        refreshStatus.style.display = 'none';
                    }, 3000);
                }
                return;
            }

            scriptSettings.apiKey = apiKeyInput.value.trim();
            fetchTornItems(true);
        }
    });

    function observeUserMenu() {
        const menuObserver = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (mutation.addedNodes.length > 0) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('settings-menu')) {
                            addSettingsMenuItem();
                            break;
                        }
                    }
                }
            });
        });
        menuObserver.observe(document.body, { childList: true, subtree: true });
        if (document.querySelector('.settings-menu')) {
            addSettingsMenuItem();
        }
    }
    observeUserMenu();

    function getSortOrderForKey(key) {
        return key === "price" ? "asc" : "desc";
    }

    function cleanupResources() {
        if (observer) {
            observer.disconnect();
        }
        if (bodyObserver) {
            bodyObserver.disconnect();
        }
        document.querySelectorAll('.bazaar-scroll-container').forEach(container => {
            const scrollWrapper = container.querySelector('.bazaar-scroll-wrapper');
            if (scrollWrapper && scrollWrapper.isScrolling) {
                cancelAnimationFrame(scrollWrapper.scrollAnimationId);
            }
        });
    }
    window.addEventListener('beforeunload', cleanupResources);
})();

QingJ © 2025

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