Torn Bazaar Quick Pricer

Auto-fill bazaar items with market-based pricing (PDA optimized)

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn Bazaar Quick Pricer
// @namespace    http://tampermonkey.net/
// @version      2.6
// @description  Auto-fill bazaar items with market-based pricing (PDA optimized)
// @author       Zedtrooper [3028329]
// @license      MIT
// @match        https://www.torn.com/bazaar.php*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// @run-at       document-end
// @homepage     https://github.com/Musa-dabwe/Torn-Bazaar-Quick-Pricer
// @supportURL   https://github.com/Musa-dabwe/Torn-Bazaar-Quick-Pricer/issues
// ==/UserScript==

(function() {
    'use strict';

    console.log('[BazaarQuickPricer] v2.6 Starting (PDA optimized)...');

    // Configuration
    const CONFIG = {
        defaultDiscount: GM_getValue('discountPercent', 0),
        apiKey: GM_getValue('tornApiKey', ''),
        lastPriceUpdate: GM_getValue('lastPriceUpdate', 0),
        priceCache: GM_getValue('priceCache', {}),
        cacheTimeout: 5 * 60 * 1000
    };

    const processedItems = new WeakSet();
    let mutationDebounceTimer = null;
    const isMobile = window.innerWidth <= 784;
    let buttonsAdded = false;

    // Detect dark mode
    function isDarkMode() {
        return document.body.classList.contains('dark-mode');
    }

    // Get appropriate text color based on theme
    function getTextColor() {
        return isDarkMode() ? '#767676' : '#7F7F7F';
    }

    function saveConfig() {
        GM_setValue('discountPercent', CONFIG.defaultDiscount);
        GM_setValue('tornApiKey', CONFIG.apiKey);
        GM_setValue('lastPriceUpdate', CONFIG.lastPriceUpdate);
        GM_setValue('priceCache', CONFIG.priceCache);
    }

    // Custom SVGs
    const addButtonSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M3,7.5v11c0,1.38,1.12,2.5,2.5,2.5h1c.83,0,1.5,.67,1.5,1.5s-.67,1.5-1.5,1.5h-1c-3.03,0-5.5-2.47-5.5-5.5V7.5C0,4.47,2.47,2,5.5,2h.35c.56-1.18,1.76-2,3.15-2h2c1.39,0,2.59,.82,3.15,2h.35c1.96,0,3.78,1.05,4.76,2.75,.42,.72,.17,1.63-.55,2.05-.24,.14-.49,.2-.75,.2-.52,0-1.02-.27-1.3-.75-.45-.77-1.28-1.25-2.17-1.25h-.35c-.56,1.18-1.76,2-3.15,2h-2c-1.39,0-2.59-.82-3.15-2h-.35c-1.38,0-2.5,1.12-2.5,2.5Zm14.5,6.5h-1c-.83,0-1.5,.67-1.5,1.5s.67,1.5,1.5,1.5h1c.83,0,1.5-.67,1.5-1.5s-.67-1.5-1.5-1.5Zm6.5-.5v6c0,2.48-2.02,4.5-4.5,4.5h-5c-2.48,0-4.5-2.02-4.5-4.5v-6c0-2.48,2.02-4.5,4.5-4.5h5c2.48,0,4.5,2.02,4.5,4.5Zm-3,0c0-.83-.67-1.5-1.5-1.5h-5c-.83,0-1.5,.67-1.5,1.5v6c0,.83,.67,1.5,1.5,1.5h5c.83,0,1.5-.67,1.5-1.5v-6Z"/></svg>`;

    function showApiKeyPrompt() {
        const overlay = document.createElement('div');
        overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:99999;display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box;';
        overlay.innerHTML = `
            <div style="background:#2a2a2a;padding:25px;border-radius:8px;max-width:400px;width:100%;color:#fff;">
                <h2 style="margin:0 0 15px 0;color:#fff;font-size:18px;">Quick Pricer Setup</h2>
                <p style="margin:0 0 15px 0;line-height:1.5;font-size:14px;">Enter your <strong>Public API Key</strong>:</p>
                <input type="text" id="apiKeyInput" placeholder="API Key" style="width:100%;padding:10px;margin:10px 0;border:1px solid #555;border-radius:5px;box-sizing:border-box;background:#1a1a1a;color:#fff;font-size:14px;">
                <div style="display:flex;gap:10px;margin-top:15px;">
                    <button id="saveApiKey" style="flex:1;padding:10px;background:#4CAF50;color:white;border:none;border-radius:5px;cursor:pointer;font-size:14px;">Save</button>
                    <button id="cancelApiKey" style="flex:1;padding:10px;background:#f44336;color:white;border:none;border-radius:5px;cursor:pointer;font-size:14px;">Cancel</button>
                </div>
            </div>
        `;
        document.body.appendChild(overlay);

        document.getElementById('saveApiKey').onclick = () => {
            const key = document.getElementById('apiKeyInput').value.trim();
            if (key && key.length === 16) {
                CONFIG.apiKey = key;
                saveConfig();
                overlay.remove();
                location.reload();
            } else {
                alert('Please enter a valid 16-character API key');
            }
        };
        document.getElementById('cancelApiKey').onclick = () => overlay.remove();
    }

    function showSettingsPanel() {
        const overlay = document.createElement('div');
        overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);z-index:99999;display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box;';
        overlay.innerHTML = `
            <div style="background:#2a2a2a;padding:25px;border-radius:8px;max-width:400px;width:100%;color:#fff;">
                <h2 style="margin:0 0 15px 0;font-size:18px;">Quick Pricer Settings</h2>
                <div style="margin:15px 0;">
                    <label style="display:block;margin-bottom:5px;font-weight:bold;font-size:14px;">Discount %:</label>
                    <input type="number" id="discountInput" value="${CONFIG.defaultDiscount}" min="-50" max="50" step="0.5" style="width:100%;padding:10px;border:1px solid #555;border-radius:5px;background:#1a1a1a;color:#fff;font-size:14px;">
                    <small style="color:#999;font-size:11px;display:block;margin-top:5px;">Use negative values to price above market (e.g., -5 for +5%)</small>
                </div>
                <div style="margin:15px 0;">
                    <label style="display:block;margin-bottom:5px;font-weight:bold;font-size:14px;">API Key:</label>
                    <input type="text" id="apiKeyUpdateInput" value="${CONFIG.apiKey}" style="width:100%;padding:10px;border:1px solid #555;border-radius:5px;background:#1a1a1a;color:#fff;font-size:14px;">
                </div>
                <button id="clearCache" style="width:100%;padding:10px;background:#ff9800;color:white;border:none;border-radius:5px;cursor:pointer;font-size:14px;margin:10px 0;">Clear Cache</button>
                <div style="display:flex;gap:10px;margin-top:15px;">
                    <button id="saveSettings" style="flex:1;padding:10px;background:#4CAF50;color:white;border:none;border-radius:5px;cursor:pointer;font-size:14px;">Save</button>
                    <button id="cancelSettings" style="flex:1;padding:10px;background:#999;color:white;border:none;border-radius:5px;cursor:pointer;font-size:14px;">Cancel</button>
                </div>
                <div style="margin-top:15px;padding-top:15px;border-top:1px solid #555;text-align:center;">
                    <small style="color:#999;font-size:12px;">
                        v2.6 | <a href="https://github.com/Musa-dabwe/Torn-Bazaar-Quick-Pricer" target="_blank" style="color:#2196F3;">GitHub</a>
                    </small>
                </div>
            </div>
        `;
        document.body.appendChild(overlay);

        document.getElementById('clearCache').onclick = () => {
            CONFIG.priceCache = {};
            CONFIG.lastPriceUpdate = 0;
            saveConfig();
            alert('Cache cleared!');
        };
        document.getElementById('saveSettings').onclick = () => {
            CONFIG.defaultDiscount = parseFloat(document.getElementById('discountInput').value);
            CONFIG.apiKey = document.getElementById('apiKeyUpdateInput').value.trim();
            saveConfig();
            overlay.remove();
            alert('Settings saved!');
        };
        document.getElementById('cancelSettings').onclick = () => overlay.remove();
    }

    const itemIdCache = new Map();
    function getItemIdFromImage(image) {
        const src = image.src;
        if (itemIdCache.has(src)) return itemIdCache.get(src);
        const match = src.match(/\/(\d+)\//);
        if (match) {
            const itemId = parseInt(match[1], 10);
            itemIdCache.set(src, itemId);
            return itemId;
        }
        return null;
    }

    function getQuantity(itemElement) {
        const titleWrap = itemElement.querySelector('div.title-wrap');
        if (!titleWrap) return 1;
        const match = titleWrap.textContent.match(/x(\d+)/i);
        return match ? parseInt(match[1], 10) : 1;
    }

    const requestQueue = [];
    let isProcessingQueue = false;

    function processRequestQueue() {
        if (isProcessingQueue || requestQueue.length === 0) return;
        isProcessingQueue = true;
        const { itemId, callback } = requestQueue.shift();
        
        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://api.torn.com/torn/${itemId}?selections=items&key=${CONFIG.apiKey}`,
            onload: function(response) {
                try {
                    const data = JSON.parse(response.responseText);
                    if (data.error) {
                        if (data.error.code === 2) {
                            alert('Incorrect API Key!');
                            CONFIG.apiKey = null;
                            saveConfig();
                        }
                        callback({ marketValue: 0, sellPrice: 0 });
                    } else if (data.items?.[itemId]) {
                        const itemData = data.items[itemId];
                        const marketValue = itemData.market_value || 0;
                        const sellPrice = itemData.sell_price || 0;
                        
                        CONFIG.priceCache[itemId] = {
                            marketValue: marketValue,
                            sellPrice: sellPrice,
                            timestamp: Date.now()
                        };
                        CONFIG.lastPriceUpdate = Date.now();
                        saveConfig();
                        
                        callback({ marketValue, sellPrice });
                    } else {
                        callback({ marketValue: 0, sellPrice: 0 });
                    }
                } catch (e) {
                    console.error('[BazaarQuickPricer] Parse error:', e);
                    callback({ marketValue: 0, sellPrice: 0 });
                }
                isProcessingQueue = false;
                setTimeout(processRequestQueue, 300);
            },
            onerror: function() {
                callback({ marketValue: 0, sellPrice: 0 });
                isProcessingQueue = false;
                setTimeout(processRequestQueue, 300);
            }
        });
    }

    function fetchItemData(itemId, callback) {
        const now = Date.now();
        const cached = CONFIG.priceCache[itemId];
        
        if (cached && cached.timestamp && (now - cached.timestamp < CONFIG.cacheTimeout)) {
            callback({
                marketValue: cached.marketValue,
                sellPrice: cached.sellPrice
            });
            return;
        }
        
        requestQueue.push({ itemId, callback });
        processRequestQueue();
    }

    function calculateFinalPrice(marketValue, sellPrice, discount) {
        let finalPrice = Math.round(marketValue * (1 - discount / 100));
        
        if (sellPrice > 0 && finalPrice < sellPrice) {
            console.log(`[BazaarQuickPricer] Price ${finalPrice} below NPC sell price ${sellPrice}, adjusting...`);
            finalPrice = sellPrice;
        }
        
        return finalPrice;
    }

    function fillItemPrice(itemElement) {
        const image = itemElement.querySelector('div.image-wrap img');
        if (!image) return Promise.resolve();

        const itemId = getItemIdFromImage(image);
        if (!itemId) return Promise.resolve();

        const amountDiv = itemElement.querySelector('div.amount-main-wrap');
        if (!amountDiv) return Promise.resolve();

        const priceInputs = amountDiv.querySelectorAll('div.price div input');
        if (priceInputs.length === 0) return Promise.resolve();

        return new Promise((resolve) => {
            fetchItemData(itemId, ({ marketValue, sellPrice }) => {
                if (marketValue > 0) {
                    const finalPrice = calculateFinalPrice(marketValue, sellPrice, CONFIG.defaultDiscount);
                    
                    priceInputs[0].value = finalPrice;
                    priceInputs[1].value = finalPrice;
                    priceInputs[0].dispatchEvent(new Event('input', { bubbles: true }));

                    const isQuantityCheckbox = amountDiv.querySelector('div.amount.choice-container');
                    if (isQuantityCheckbox) {
                        const checkbox = isQuantityCheckbox.querySelector('input');
                        if (checkbox && !checkbox.checked) checkbox.click();
                    } else {
                        const quantityInput = amountDiv.querySelector('div.amount input');
                        if (quantityInput) {
                            quantityInput.value = getQuantity(itemElement);
                            quantityInput.dispatchEvent(new Event('input', { bubbles: true }));
                            quantityInput.dispatchEvent(new Event('keyup', { bubbles: true }));
                        }
                    }
                }
                resolve();
            });
        });
    }

    function getActiveTab() {
        // Find the currently active/visible tab
        const tabs = document.querySelectorAll('ul.items-tabs li');
        for (const tab of tabs) {
            if (tab.classList.contains('active')) {
                return tab.getAttribute('data-category') || 'all';
            }
        }
        return 'all';
    }

    function getVisibleItems() {
        // Get only items in the currently active tab/category
        const activeTab = getActiveTab();
        const allItemsLists = document.querySelectorAll('ul.items-cont');
        
        for (const list of allItemsLists) {
            // Check if this list is visible (not display:none)
            const style = window.getComputedStyle(list);
            if (style.display !== 'none') {
                const items = list.querySelectorAll('li.clearfix:not(.disabled)');
                return Array.from(items);
            }
        }
        
        return [];
    }

    function addQuickPriceButton(itemElement) {
        if (processedItems.has(itemElement)) return;
        
        const titleWrap = itemElement.querySelector('div.title-wrap');
        if (!titleWrap) return;

        if (titleWrap.querySelector('.quick-price-btn')) {
            processedItems.add(itemElement);
            return;
        }

        processedItems.add(itemElement);

        const image = itemElement.querySelector('div.image-wrap img');
        if (!image) return;

        const itemId = getItemIdFromImage(image);
        if (!itemId) return;

        const amountDiv = itemElement.querySelector('div.amount-main-wrap');
        if (!amountDiv) return;

        const priceInputs = amountDiv.querySelectorAll('div.price div input');
        if (priceInputs.length === 0) return;

        const btnContainer = document.createElement('div');
        btnContainer.className = 'quick-price-btn';
        btnContainer.style.cssText = 'position:absolute;right:10px;top:50%;transform:translateY(-50%);z-index:10;';

        const btnInput = document.createElement('button');
        btnInput.innerHTML = addButtonSVG;
        btnInput.style.cssText = 'background:#5F5F5F;color:white;padding:8px;border:none;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 4px rgba(0,0,0,0.2);transition:background 0.2s;';
        btnInput.setAttribute('title', 'Quick Add');

        btnInput.addEventListener('mouseenter', () => {
            btnInput.style.background = '#4F4F4F';
        });
        btnInput.addEventListener('mouseleave', () => {
            if (!btnInput.disabled) btnInput.style.background = '#5F5F5F';
        });

        btnContainer.appendChild(btnInput);
        titleWrap.style.position = 'relative';
        titleWrap.appendChild(btnContainer);

        btnInput.addEventListener('click', function(event) {
            event.stopPropagation();
            btnInput.disabled = true;
            btnInput.style.opacity = '0.5';

            // Use Promise to handle async operation
            fillItemPrice(itemElement).then(() => {
                btnInput.disabled = false;
                btnInput.style.opacity = '1';
            });
        });
    }

    async function fillAllItems() {
        const items = getVisibleItems();
        console.log('[BazaarQuickPricer] Filling', items.length, 'items in current tab simultaneously...');
        
        if (items.length === 0) {
            alert('No items found to fill!');
            return;
        }

        const fillButton = document.getElementById('quickFillAllBtn');
        if (fillButton) {
            fillButton.disabled = true;
            fillButton.style.opacity = '0.5';
            fillButton.textContent = 'Filling...';
        }

        // Process all visible items simultaneously
        const promises = items.map(item => fillItemPrice(item));
        await Promise.all(promises);

        if (fillButton) {
            fillButton.disabled = false;
            fillButton.style.opacity = '1';
            fillButton.textContent = 'Quick Fill';
        }

        console.log('[BazaarQuickPricer] Fill complete!');
    }

    function addTopButtons() {
        if (buttonsAdded) return;

        let attempts = 0;
        const maxAttempts = 20;

        const tryAddButtons = setInterval(() => {
            attempts++;
            const titleSection = document.querySelector('div.title-black');

            if (titleSection && titleSection.textContent.includes('Add items to your Bazaar')) {
                if (document.getElementById('quickFillAllBtn')) {
                    clearInterval(tryAddButtons);
                    buttonsAdded = true;
                    return;
                }

                clearInterval(tryAddButtons);
                buttonsAdded = true;

                const buttonContainer = document.createElement('div');
                buttonContainer.style.cssText = 'display:inline-flex;margin-left:15px;vertical-align:top;align-items:flex-start;';

                // Quick Fill button (text only, no icon)
                const fillAllBtn = document.createElement('button');
                fillAllBtn.id = 'quickFillAllBtn';
                fillAllBtn.textContent = 'Quick Fill';
                fillAllBtn.style.cssText = 'background:#5F5F5F;color:white;padding:8px 14px;border:none;border-radius:4px 0 0 4px;cursor:pointer;display:inline-flex;align-items:center;font-size:13px;box-shadow:0 2px 4px rgba(0,0,0,0.2);transition:background 0.2s;border-right:1px solid #4F4F4F;';
                fillAllBtn.setAttribute('title', 'Fill all items in current tab with market prices');

                fillAllBtn.addEventListener('mouseenter', () => {
                    if (!fillAllBtn.disabled) fillAllBtn.style.background = '#4F4F4F';
                });
                fillAllBtn.addEventListener('mouseleave', () => {
                    if (!fillAllBtn.disabled) fillAllBtn.style.background = '#5F5F5F';
                });
                fillAllBtn.addEventListener('click', fillAllItems);

                // Settings button
                const settingsBtn = document.createElement('button');
                settingsBtn.id = 'quickPricerSettingsBtn';
                settingsBtn.textContent = 'Settings';
                settingsBtn.style.cssText = 'background:#5F5F5F;color:white;padding:8px 14px;border:none;border-radius:0 4px 4px 0;cursor:pointer;display:inline-flex;align-items:center;font-size:13px;box-shadow:0 2px 4px rgba(0,0,0,0.2);transition:background 0.2s;';
                settingsBtn.setAttribute('title', 'Open Quick Pricer settings');

                settingsBtn.addEventListener('mouseenter', () => {
                    settingsBtn.style.background = '#4F4F4F';
                });
                settingsBtn.addEventListener('mouseleave', () => {
                    settingsBtn.style.background = '#5F5F5F';
                });
                settingsBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    showSettingsPanel();
                });

                buttonContainer.appendChild(fillAllBtn);
                buttonContainer.appendChild(settingsBtn);
                titleSection.appendChild(buttonContainer);

                console.log('[BazaarQuickPricer] Buttons added');
            } else if (attempts >= maxAttempts) {
                clearInterval(tryAddButtons);
                console.log('[BazaarQuickPricer] Buttons failed to add');
            }
        }, 500);
    }

    function processAllItems() {
        const items = document.querySelectorAll('ul.items-cont li.clearfix:not(.disabled)');
        console.log('[BazaarQuickPricer] Found', items.length, 'items');
        if (items.length > 0) {
            items.forEach(item => addQuickPriceButton(item));
        }
    }

    function setupObserver() {
        const bazaarRoot = document.getElementById('bazaarRoot');
        if (!bazaarRoot) {
            setTimeout(setupObserver, 1000);
            return;
        }

        console.log('[BazaarQuickPricer] Observer starting');
        const observer = new MutationObserver(() => {
            clearTimeout(mutationDebounceTimer);
            mutationDebounceTimer = setTimeout(() => {
                processAllItems();
                addTopButtons();
            }, 300);
        });

        observer.observe(bazaarRoot, { childList: true, subtree: true });
    }

    function init() {
        console.log('[BazaarQuickPricer] Init starting');
        
        if (!CONFIG.apiKey || CONFIG.apiKey === 'null') {
            setTimeout(showApiKeyPrompt, 1000);
            return;
        }

        setTimeout(() => {
            processAllItems();
            setupObserver();
            addTopButtons();
        }, 2000);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        setTimeout(init, 1000);
    }

})();