Torn Junk Seller

Automates pricing with support for fixed prices, API, and RRP.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         Torn Junk Seller
// @namespace    torn.tools
// @version      2.2.2
// @description  Automates pricing with support for fixed prices, API, and RRP.
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @connect      greasyfork.org
// @run-at       document-idle
// @license      CC BY-ND 4.0
// ==/UserScript==

(function () {
    'use strict';

    const CURRENT_VERSION     = '2.2.2';
    const STORAGE_KEY_MARKET  = 'tm_saved_items';
    const STORAGE_KEY_BAZAAR  = 'tm_saved_items_bazaar';
    const CONFIG_KEY          = 'tm_config';
    const KEEP_QTY_KEY_MARKET = 'tm_keep_qty_market';
    const KEEP_QTY_KEY_BAZAAR = 'tm_keep_qty_bazaar';
    const ITEM_MARGIN_KEY     = 'tm_item_margin';
    const PRICE_CACHE_KEY     = 'tm_price_cache';
    const LOCKED_PRICES_KEY   = 'tm_locked_prices';
    const FIXED_PRICE_KEY     = 'tm_fixed_prices';

    const BULK_THRESHOLD = 200;
    const BULK_DISCOUNT  = 0.10;
    const RRP_FALLBACK_THRESHOLD = 0.20;

    function getInfoEl(row) {
        const itemRow = row.querySelector('[class*="itemRow"]');
        if (!itemRow) return null;
        const children = [...itemRow.children];
        return children.find(el => el.tagName !== 'BUTTON') ?? null;
    }

    function getPriceWrapper(row) {
        const priceInput = row.querySelector('input[aria-label*=" price"]');
        return priceInput?.closest('[class*="priceInputWrapper"]') ?? null;
    }

    function getNameEl(row) {
        return row.querySelector('[class*="name"]') ?? row.querySelector('[class*="title"]');
    }

    /* ===============================
       PAGE DETECTION
    =============================== */

    function isAddListingPage() {
        const hash = window.location.hash;
        return (hash.includes('/addListing') || hash.includes('/add')) && !hash.includes('/viewListing') && !hash.includes('/confirm');
    }

    function isConfirmPage() {
        return window.location.hash.includes('/addListing/confirm');
    }

    function isViewListingPage() {
        return window.location.hash.includes('/viewListing');
    }

    function isBazaarPage() {
        return window.location.pathname.includes('bazaar.php') && window.location.hash.includes('/add');
    }

    function isManageBazaarPage() {
        return window.location.pathname.includes('bazaar.php') && window.location.hash.includes('/manage');
    }

    function isActivePage() {
        return isAddListingPage() || isBazaarPage() || isManageBazaarPage() || isViewListingPage() || isConfirmPage();
    }

    /* ===============================
       CONFIG & STORAGE
    =============================== */

    let _configCache = null;
    function getConfig() {
        if (_configCache) return _configCache;
        const defaults = { margin: 5, rounding: 50, apiKey: '', cacheTtlMin: 5, bulkDetection: true, bulkThreshold: 200 };
        try {
            const cfg = localStorage.getItem(CONFIG_KEY);
            _configCache = cfg ? Object.assign({}, defaults, JSON.parse(cfg)) : defaults;
        } catch { _configCache = defaults; }
        return _configCache;
    }

    function saveConfig(cfg) {
        _configCache = null;
        _playerIdCache = null;
        localStorage.setItem(CONFIG_KEY, JSON.stringify(cfg));
    }

    function getMargin()          { return getConfig().margin ?? 5; }
    function getRounding()        { return getConfig().rounding ?? 50; }
    function getApiKey()          { return getConfig().apiKey ?? ''; }
    function getCacheTtlMs()      { return (getConfig().cacheTtlMin ?? 5) * 60 * 1000; }
    function isBulkDetectionOn()  { return getConfig().bulkDetection !== false; }
    function getBulkThreshold()   { return getConfig().bulkThreshold ?? BULK_THRESHOLD; }

    let _fixedPriceCache = null;
    function getFixedPriceMap() {
        if (_fixedPriceCache) return _fixedPriceCache;
        try { _fixedPriceCache = JSON.parse(localStorage.getItem(FIXED_PRICE_KEY)) || {}; }
        catch { _fixedPriceCache = {}; }
        return _fixedPriceCache;
    }

    function getFixedPrice(id) { return getFixedPriceMap()[id] ?? null; }

    function setFixedPrice(id, value) {
        const map = getFixedPriceMap();
        if (value === null || value === '' || isNaN(value)) delete map[id];
        else map[id] = Number(value);
        _fixedPriceCache = map;
        localStorage.setItem(FIXED_PRICE_KEY, JSON.stringify(map));
    }

    /* ===============================
       PRICE CACHE
    =============================== */

    let _priceCache = null;
    function loadPriceCache() {
        if (_priceCache) return _priceCache;
        try { _priceCache = JSON.parse(localStorage.getItem(PRICE_CACHE_KEY)) || {}; }
        catch { _priceCache = {}; }
        return _priceCache;
    }

    function savePriceCache(cache) {
        _priceCache = cache;
        localStorage.setItem(PRICE_CACHE_KEY, JSON.stringify(cache));
    }

    function getCacheEntry(itemId) {
        const cache = loadPriceCache();
        const entry = cache[itemId];
        if (!entry) return null;
        if (Date.now() - entry.ts > getCacheTtlMs()) return null;
        return entry;
    }

    function setCacheEntry(itemId, entry) {
        const cache = loadPriceCache();
        cache[itemId] = { ...entry, ts: Date.now() };
        const ttl = getCacheTtlMs();
        const now = Date.now();
        for (const id of Object.keys(cache)) {
            if (now - cache[id].ts > ttl) delete cache[id];
        }
        savePriceCache(cache);
    }

    function clearCacheEntry(itemId) {
        const cache = loadPriceCache();
        delete cache[itemId];
        savePriceCache(cache);
    }

    /* ===============================
       LOCKED PRICES
    =============================== */

    let _lockedPricesCache = null;
    function getLockedPrices() {
        if (_lockedPricesCache) return _lockedPricesCache;
        try { _lockedPricesCache = JSON.parse(localStorage.getItem(LOCKED_PRICES_KEY)) || []; }
        catch { _lockedPricesCache = []; }
        return _lockedPricesCache;
    }

    function isPriceLocked(itemId) { return getLockedPrices().includes(itemId); }

    function togglePriceLock(itemId) {
        const locked = getLockedPrices();
        const idx = locked.indexOf(itemId);
        if (idx === -1) locked.push(itemId);
        else locked.splice(idx, 1);
        _lockedPricesCache = locked;
        localStorage.setItem(LOCKED_PRICES_KEY, JSON.stringify(locked));
        return idx === -1;
    }

    /* ===============================
       PLAYER ID
    =============================== */

    let _playerIdCache = null;
    async function getPlayerId() {
        if (_playerIdCache) return _playerIdCache;
        const key = getApiKey();
        if (!key) return null;
        try {
            const resp = await fetch(`https://api.torn.com/v2/user?key=${key}&fields=basic`);
            if (!resp.ok) return null;
            const data = await resp.json();
            if (data.error || !data.id) return null;
            _playerIdCache = data.id;
            return _playerIdCache;
        } catch { return null; }
    }

    /* ===============================
       TORN API
    =============================== */

    async function fetchMarketData(itemId) {
        const cached = getCacheEntry(itemId);
        if (cached) return cached;

        const key = getApiKey();
        if (!key) return null;

        try {
            const url = `https://api.torn.com/v2/market/${itemId}/itemmarket?key=${key}&limit=10`;
            const resp = await fetch(url);
            if (!resp.ok) return null;
            const data = await resp.json();
            if (data.error) return null;

            const allListings = data?.itemmarket?.listings;
            if (!allListings || allListings.length === 0) return null;

            const playerId = await getPlayerId();
            const listings = playerId
                ? allListings.filter(l => l.seller?.id !== playerId)
                : allListings;

            if (listings.length === 0) return null;

            const bulkThreshold = getBulkThreshold();
            const lowest = listings[0].price;
            let bulk = null;
            for (const l of listings) {
                if (l.amount >= bulkThreshold) {
                    if (!bulk || l.price < bulk.price) bulk = { price: l.price, amount: l.amount };
                }
            }

            const entry = { lowest, bulk };
            setCacheEntry(itemId, entry);
            return entry;
        } catch {
            return null;
        }
    }

    /* ===============================
       UPDATE CHECKER (Bypass CSP)
    =============================== */

    function checkVersion() {
        if (!window.location.href.includes('page.php?sid=ItemMarket') || !window.location.hash.includes('/addListing')) {
            return;
        }

        if (typeof GM_xmlhttpRequest === 'undefined') {
            console.error('[Junk Seller] GM_xmlhttpRequest nie jest dostepny.');
            return;
        }

        GM_xmlhttpRequest({
            method: 'GET',
            url: 'https://greasyfork.org/en/scripts/570131-torn-junk-seller.json',
            onload: function (response) {
                if (response.status !== 200) return;
                try {
                    const data = JSON.parse(response.responseText);
                    if (data && data.version && data.version !== CURRENT_VERSION) {
                        createUpdateWidget(data.version);
                    }
                } catch (e) {
                    console.error('[Junk Seller] Blad parsowania JSON z aktualizacja:', e);
                }
            },
            onerror: function (e) {
                console.error('[Junk Seller] Blad polaczenia z GreasyFork:', e);
            }
        });
    }

    function createUpdateWidget(latestVersion) {
        if (document.getElementById('tm_update_notification')) return;
        if (isConfirmPage()) return;

        const btn = document.createElement('a');
        btn.id = 'tm_update_notification';
        btn.href = 'https://greasyfork.org/en/scripts/570131-torn-junk-seller';
        btn.target = '_blank';
        btn.title = `Wersja w repozytorium (${latestVersion}) rozni sie od Twojej aktualnej (${CURRENT_VERSION})! Kliknij, aby zaktualizowac.`;
        btn.classList.add('tm-injected');
        btn.style.cssText = `
            width: 24px; position:fixed;right:0;bottom:calc(20% - 22px);z-index:9999;cursor:pointer;
            background:linear-gradient(90deg, #4CAF50, #2E7D32);
            padding:3px 8px;border-radius:4px 0 0 4px;display:flex;align-items:center;justify-content:center;
            box-shadow: -2px 2px 6px rgba(0,0,0,0.4);text-decoration:none;
        `;
        btn.innerHTML = `<span style="color:white;font-family:Arial,sans-serif;font-size:10px;font-weight:bold;letter-spacing:0.5px;text-align: right;">update</span>`;

        document.body.appendChild(btn);
    }

    /* ===============================
       PRICE LOGIC
    =============================== */

    function parseMoney(text) {
        if (!text) return 0;
        return Number(text.replace('$', '').replace(/,/g, '').trim());
    }

    function roundToNearest(value) {
        const r = getRounding();
        if (!r || r <= 0) return Math.round(value);
        return Math.round(value / r) * r;
    }

    function getItemMarginPct(itemId) {
        return itemId !== undefined ? (getItemMargin(itemId) ?? getMargin()) : getMargin();
    }

    function computePrice(rrp, apiData, itemId) {
        const fixedPrice = itemId ? getFixedPrice(itemId) : null;
        if (fixedPrice !== null && fixedPrice > 0) {
            return {
                price: fixedPrice,
                tooltip: `Fixed Price: $${fmt(fixedPrice)} (Manual Override)`,
            };
        }

        const marginPct = getItemMarginPct(itemId);
        const ts = apiData ? fmtTime(getCacheEntry(itemId)?.ts ?? Date.now()) : null;
        const bulkEnabled = isBulkDetectionOn();
        const bulkThreshold = getBulkThreshold();

        if (!apiData) {
            const price = roundToNearest(rrp * (1 + marginPct / 100));
            const marginAmt = price - rrp;
            return {
                price,
                tooltip: `RRP: $${fmt(rrp)} + $${fmt(marginAmt)} (+${marginPct}%) = $${fmt(price)}`,
            };
        }

        const { lowest, bulk } = apiData;
        let basePrice, usedRrpFallback = false;

        if (bulkEnabled && bulk) {
            basePrice = bulk.price * (1 - BULK_DISCOUNT);
        } else {
            basePrice = lowest;
        }

        if (!basePrice || basePrice < rrp * RRP_FALLBACK_THRESHOLD) {
            basePrice = rrp;
            usedRrpFallback = true;
        }

        const price = roundToNearest(basePrice * (1 + marginPct / 100));
        const marginAmt = price - Math.round(basePrice);

        const source = usedRrpFallback ? 'RRP' : 'API';
        const baseLabel = usedRrpFallback ? fmt(rrp) : fmt(lowest);
        let line = `${source} (${ts}): $${baseLabel} + $${fmt(marginAmt)} (+${marginPct}%) = $${fmt(price)}`;

        if (bulkEnabled && bulk) {
            line += `\n(Bulk seller: ${fmt(bulk.amount)}x $${fmt(bulk.price)} - -${BULK_DISCOUNT * 100}% - base $${fmt(Math.round(basePrice))})`;
        } else if (!bulkEnabled && bulk) {
            line += `\n(Bulk seller detected but ignored - bulk detection disabled)`;
        } else if (usedRrpFallback) {
            line += `\n(API $${fmt(lowest)} < RRP $${fmt(rrp)} = using RRP)`;
        } else if (bulkEnabled && !bulk) {
            line += `\n(No bulk seller found - threshold: ${fmt(bulkThreshold)}+ items)`;
        }

        return { price, tooltip: line };
    }

    function fmt(n) {
        return Number(n).toLocaleString('en-US');
    }

    function fmtTime(ts) {
        return new Date(ts).toTimeString().slice(0, 5);
    }

    /* ===============================
       ITEM MARKET - RRP + apply
    =============================== */

    function getRRP_Market(row) {
        const priceContainer = row.querySelector('[aria-label*="Recommended Retail Price"]');
        if (!priceContainer) return 0;
        const visiblePrice = priceContainer.querySelector('span');
        if (!visiblePrice) return 0;
        return parseMoney(visiblePrice.textContent);
    }

    function applyPriceToRow_Market(row, price, tooltip, priceOnly = false) {
        const expandBtn = row.querySelector('button[aria-expanded="false"] .group-arrow');
        const groupExpandBtn = expandBtn?.closest('button');

        if (groupExpandBtn) {
            groupExpandBtn.click();
            setTimeout(() => applyPriceToRow_Market(row, price, tooltip, priceOnly), 300);
            return;
        }

        const selectCheckbox = row.querySelector('input[id^="itemRow-selectCheckbox"]');
        if (!priceOnly && selectCheckbox && !selectCheckbox.checked) selectCheckbox.click();

        const inputs = row.querySelectorAll('input[data-testid="legacy-money-input"]:not([type="hidden"])');
        if (!inputs.length) return;

        const itemId = getItemId(row);

        inputs.forEach(input => {
            const hidden = input.parentElement.querySelector(
                'input[data-testid="legacy-money-input"][type="hidden"]'
            );

            let value;
            const dataMoney = input.dataset.money;

            if (dataMoney === 'Infinity') {
                value = price;
                if (tooltip) {
                    const wrap = input.closest('[class*="priceInput"]') || input.parentElement;
                    wrap.title = tooltip;
                }
            } else {
                if (priceOnly) return;
                if (selectCheckbox) return;
                const maxQty = Number(dataMoney);
                const keepQty = itemId ? getKeepQty(itemId) : null;
                value = (keepQty !== null && keepQty >= 0) ? Math.max(0, maxQty - keepQty) : maxQty;
            }

            setReactInputValue(input, value);
            if (hidden) hidden.value = value;
        });
    }

    async function setValue_Market(row, priceOnly = false) {
        const itemId = getItemId(row);
        const rrp = getRRP_Market(row);
        const apiData = itemId ? await fetchMarketData(itemId) : null;
        const { price, tooltip } = computePrice(rrp, apiData, itemId);
        applyPriceToRow_Market(row, price, tooltip, priceOnly);
    }

    /* ===============================
       VIEW LISTING - row updates
    =============================== */

    function refreshNameTitle(row) {
        const itemId = getItemId(row);
        if (!itemId) return;
        const cached = getCacheEntry(itemId);
        const nameEl = getNameEl(row);
        if (!nameEl) return;
        if (cached) {
            nameEl.title = `Market lowest: $${fmt(cached.lowest)}${cached.bulk ? ` | Bulk ${fmt(cached.bulk.amount)}x @ $${fmt(cached.bulk.price)}` : ''}`;
        } else {
            nameEl.removeAttribute('title');
        }
    }

    function addButton_ViewListing(row) {
        const info = getInfoEl(row);
        if (!info || info.dataset.tmViewAdded) return;
        info.dataset.tmViewAdded = '1';

        ensureStyles();
        const itemId = getItemId(row);
        refreshNameTitle(row);

        const fixedInput = document.createElement('input');
        fixedInput.type = 'text';
        fixedInput.placeholder = 'Fix $';
        fixedInput.title = 'Fixed price (overrides API/RRP)';
        fixedInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(fixedInput);
        fixedInput.style.marginLeft = '6px';
        if (itemId) {
            const savedFixed = getFixedPrice(itemId);
            if (savedFixed !== null) fixedInput.value = savedFixed;
        }
        fixedInput.addEventListener('click', e => e.stopPropagation());
        fixedInput.addEventListener('change', () => {
            if (!itemId) return;
            setFixedPrice(itemId, fixedInput.value.trim() || null);
            setValue_Market(row, true);
        });

        const marginInput = document.createElement('input');
        marginInput.type = 'text';
        marginInput.placeholder = '%';
        marginInput.title = 'Custom margin % for this item';
        marginInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(marginInput);
        marginInput.style.marginLeft = '6px';
        if (itemId) {
            const savedMargin = getItemMargin(itemId);
            if (savedMargin !== null) marginInput.value = savedMargin;
        }
        marginInput.addEventListener('click', e => e.stopPropagation());
        marginInput.addEventListener('change', () => {
            if (!itemId) return;
            setItemMargin(itemId, marginInput.value.trim() || null);
            clearCacheEntry(itemId);
            setValue_Market(row, true).then(() => refreshNameTitle(row));
        });

        const netLabel = document.createElement('span');
        netLabel.classList.add('tm-row-net', 'tm-injected');
        netLabel.style.cssText = 'margin-left:6px;font-size:10px;color:#8BC34A;line-height:1.2;text-align:right;vertical-align:middle;white-space:nowrap;width:55px;display:flex;align-items:center;justify-content:flex-end;';

        function refreshNetLabel() {
            const priceHidden = row.querySelector('input[data-money="Infinity"][type="hidden"]');
            const qtyHidden   = row.querySelector('input[type="hidden"]:not([data-money="Infinity"])');
            if (!priceHidden || !qtyHidden) { netLabel.textContent = ''; return; }
            const price   = Number(priceHidden.value);
            const qty     = Number(qtyHidden.value);
            if (!price || !qty) { netLabel.textContent = ''; return; }
            const isAnon  = !!row.querySelector('[class*="checkboxWrapper"][class*="active"]');
            const feeRate = isAnon ? 0.15 : 0.05;
            const net     = Math.round(price * qty * (1 - feeRate));
            netLabel.innerHTML = `$${fmt(net)}`;
            netLabel.title = fmt(Math.round(price * qty))+"$ - "+(fmt(Math.round((price * qty)*(feeRate))))+"$";
        }

        refreshNetLabel();

        const btnA = document.createElement('button');
        btnA.textContent = 'A';
        btnA.title = 'Refresh price (API)';
        btnA.style.cssText = 'padding:0 4px;cursor:pointer;color:#2196F3;margin-left:3px;';
        btnA.classList.add('tm-injected');
        btnA.addEventListener('click', e => {
            e.stopPropagation();
            if (itemId) clearCacheEntry(itemId);
            setValue_Market(row, true).then(() => { refreshNameTitle(row); refreshNetLabel(); updateViewListingSummary(); });
        });

        const btnL = document.createElement('button');
        btnL.classList.add('tm-injected');
        btnL.style.cssText = 'padding:0 2px;cursor:pointer;margin-left:3px;background:none;border:none;line-height:1;vertical-align:middle;';

        const svgLocked = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4CAF50" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`;
        const svgUnlocked = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#FFA726" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>`;

        function updateLockBtn() {
            const locked = itemId && isPriceLocked(itemId);
            btnL.innerHTML = locked ? svgLocked : svgUnlocked;
            btnL.title = locked ? 'Unlock price' : 'Lock price';
            marginInput.disabled = !!locked;
            fixedInput.disabled = !!locked;
            marginInput.style.opacity = locked ? '0.4' : '1';
            fixedInput.style.opacity = locked ? '0.4' : '1';
        }

        updateLockBtn();
        btnL.addEventListener('click', e => {
            e.stopPropagation();
            if (!itemId) return;
            togglePriceLock(itemId);
            updateLockBtn();
        });

        info.appendChild(fixedInput);
        info.appendChild(marginInput);
        info.appendChild(netLabel);
        info.appendChild(btnA);
        info.appendChild(btnL);
    }

    /* ===============================
       BAZAAR - ADD
    =============================== */

    function getRRP_Bazaar(row) {
        const infoWrap = row.querySelector('.info-wrap[title="Market value"]');
        if (!infoWrap) return 0;
        const text = infoWrap.textContent.split('|')[0].trim();
        return parseMoney(text);
    }

    function setValue_Bazaar(row) {
        const itemId = getItemId_Bazaar(row);
        const rrp = getRRP_Bazaar(row);

        const priceInput = row.querySelector('.price input.input-money:not([type="hidden"])');
        const priceHidden = row.querySelector('.price input.input-money[type="hidden"]');

        if (priceInput) {
            const { price } = computePrice(rrp, null, itemId);
            setReactInputValue(priceInput, price);
            if (priceHidden) priceHidden.value = price;
        }

        const checkbox = row.querySelector('input[type="checkbox"][name="amount"]');
        const amountInput = row.querySelector('input[type="text"][name="amount"]');

        if (checkbox) {
            if (!checkbox.checked) checkbox.click();
        } else if (amountInput) {
            const maxQty = Number(row.querySelector('.item-amount.qty')?.textContent?.trim() ?? 0);
            const keepQty = itemId ? getKeepQty(itemId) : null;
            const qty = (keepQty !== null && keepQty >= 0) ? Math.max(0, maxQty - keepQty) : maxQty;
            setReactInputValue(amountInput, qty);
        }
    }

    /* ===============================
       BAZAAR - MANAGE
    =============================== */

    function getRRP_ManageBazaar(row) {
        const rrpEl = row.querySelector('[class*="rrp"]');
        if (!rrpEl) return 0;
        return parseMoney(rrpEl.textContent);
    }

    function getItemId_ManageBazaar(row) {
        const img = row.querySelector('img[src*="/images/items/"]');
        if (!img) return null;
        const match = (img.src || img.getAttribute('srcset') || '').match(/items\/(\d+)\//);
        return match ? match[1] : null;
    }

    async function setValue_ManageBazaar(row, priceOnly = false) {
        const itemId = getItemId_ManageBazaar(row);
        const rrp = getRRP_ManageBazaar(row);
        const apiData = itemId ? await fetchMarketData(itemId) : null;
        const { price, tooltip } = computePrice(rrp, apiData, itemId);
        applyPriceToRow_ManageBazaar(row, price, tooltip);
    }

    function applyPriceToRow_ManageBazaar(row, price, tooltip) {
        const priceInput = row.querySelector('[class*="price"] input[data-testid="legacy-money-input"]:not([type="hidden"])');
        const priceHidden = row.querySelector('[class*="price"] input[data-testid="legacy-money-input"][type="hidden"]');
        if (!priceInput) return;

        if (tooltip) {
            const wrap = priceInput.closest('[class*="price"]') || priceInput.parentElement;
            wrap.title = tooltip;
        }

        setReactInputValue(priceInput, price);
        if (priceHidden) priceHidden.value = price;
    }

    function addControls_ManageBazaar(row) {
        const itemEl = row.querySelector('[class*="item"]');
        if (!itemEl || itemEl.dataset.tmManageAdded) return;
        itemEl.dataset.tmManageAdded = '1';

        ensureStyles();

        const itemId = getItemId_ManageBazaar(row);

        const wrap = document.createElement('div');
        wrap.classList.add('tm-injected');
        wrap.style.cssText = 'display:flex;align-items:center;gap:3px;margin-left:4px;';

        const fixedInput = document.createElement('input');
        fixedInput.type = 'text';
        fixedInput.placeholder = 'Fix $';
        fixedInput.title = 'Fixed price (overrides API/RRP)';
        fixedInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(fixedInput);
        if (itemId) {
            const saved = getFixedPrice(itemId);
            if (saved !== null) fixedInput.value = saved;
        }
        fixedInput.addEventListener('click', e => e.stopPropagation());
        fixedInput.addEventListener('change', () => {
            if (!itemId) return;
            setFixedPrice(itemId, fixedInput.value.trim() || null);
            setValue_ManageBazaar(row);
        });

        const marginInput = document.createElement('input');
        marginInput.type = 'text';
        marginInput.placeholder = '%';
        marginInput.title = 'Custom margin % for this item';
        marginInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(marginInput);
        if (itemId) {
            const saved = getItemMargin(itemId);
            if (saved !== null) marginInput.value = saved;
        }
        marginInput.addEventListener('click', e => e.stopPropagation());
        marginInput.addEventListener('change', () => {
            if (!itemId) return;
            setItemMargin(itemId, marginInput.value.trim() || null);
            clearCacheEntry(itemId);
            setValue_ManageBazaar(row).then(() => refreshNameTitle_ManageBazaar(row));
        });

        const btnA = document.createElement('button');
        btnA.textContent = 'A';
        btnA.title = 'Auto-price (API/RRP)';
        btnA.style.cssText = 'padding:0 4px;cursor:pointer;color:#2196F3;';
        btnA.classList.add('tm-injected');
        btnA.addEventListener('click', e => {
            e.stopPropagation();
            if (itemId) clearCacheEntry(itemId);
            setValue_ManageBazaar(row).then(() => refreshNameTitle_ManageBazaar(row));
        });

        const btnL = document.createElement('button');
        btnL.classList.add('tm-injected');
        btnL.style.cssText = 'padding:0 2px;cursor:pointer;background:none;border:none;line-height:1;vertical-align:middle;';

        const svgLocked = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4CAF50" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`;
        const svgUnlocked = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#FFA726" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>`;

        function updateLockBtn() {
            const locked = itemId && isPriceLocked(itemId);
            btnL.innerHTML = locked ? svgLocked : svgUnlocked;
            btnL.title = locked ? 'Unlock price' : 'Lock price';
            marginInput.disabled = !!locked;
            fixedInput.disabled = !!locked;
            marginInput.style.opacity = locked ? '0.4' : '1';
            fixedInput.style.opacity = locked ? '0.4' : '1';
        }

        updateLockBtn();
        btnL.addEventListener('click', e => {
            e.stopPropagation();
            if (!itemId) return;
            togglePriceLock(itemId);
            updateLockBtn();
        });

        wrap.appendChild(fixedInput);
        wrap.appendChild(marginInput);
        wrap.appendChild(btnA);
        wrap.appendChild(btnL);

        // Insert wrap into the price cell so controls sit next to the input
        const priceCell = row.querySelector('[class*="price"]');
        if (priceCell) {
            priceCell.style.display = 'flex';
            priceCell.style.alignItems = 'center';
            priceCell.appendChild(wrap);
        } else {
            itemEl.appendChild(wrap);
        }

        // Auto-apply if item is saved
        if (itemId && isItemSaved(itemId) && !isPriceLocked(itemId) && !row.dataset.tmAutoApplied) {
            row.dataset.tmAutoApplied = '1';
            setValue_ManageBazaar(row);
        }
    }

    function refreshNameTitle_ManageBazaar(row) {
        const itemId = getItemId_ManageBazaar(row);
        if (!itemId) return;
        const cached = getCacheEntry(itemId);
        const descEl = row.querySelector('[class*="desc"]');
        if (!descEl) return;
        if (cached) {
            descEl.title = `Market lowest: $${fmt(cached.lowest)}${cached.bulk ? ` | Bulk ${fmt(cached.bulk.amount)}x @ $${fmt(cached.bulk.price)}` : ''}`;
        } else {
            descEl.removeAttribute('title');
        }
    }

    function scanRows_ManageBazaar() {
        document.querySelectorAll('[data-testid="sortable-item"]').forEach(row => {
            addControls_ManageBazaar(row);
        });
    }

    function createRefreshAllWidget_ManageBazaar() {
        if (document.getElementById('tm_refresh_all_btn')) return;

        const btn = document.createElement('div');
        btn.id = 'tm_refresh_all_btn';
        btn.title = 'Refresh all bazaar prices (API)';
        btn.classList.add('tm-injected');
        btn.style.cssText = `
            position:fixed;right:0;bottom:calc(20% + 45px);z-index:9999;cursor:pointer;
            background:linear-gradient(90deg, rgb(2,36,74), rgb(3,111,201));
            padding:8px;border-radius:6px;
        `;
        btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
          <polyline points="23 4 23 10 17 10"/>
          <polyline points="1 20 1 14 7 14"/>
          <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
        </svg>`;

        btn.addEventListener('click', async () => {
            btn.style.opacity = '0.5';
            btn.style.pointerEvents = 'none';

            const rows = [...document.querySelectorAll('[data-testid="sortable-item"]')];
            for (const row of rows) {
                const itemId = getItemId_ManageBazaar(row);
                if (itemId && isPriceLocked(itemId)) continue;
                if (itemId) clearCacheEntry(itemId);
                await setValue_ManageBazaar(row);
                refreshNameTitle_ManageBazaar(row);
            }

            btn.style.opacity = '1';
            btn.style.pointerEvents = '';
        });

        document.body.appendChild(btn);
    }

    /* ===============================
       setReactInputValue
    =============================== */

    function setReactInputValue(input, value) {
        const nativeSetter = Object.getOwnPropertyDescriptor(
            window.HTMLInputElement.prototype, 'value'
        ).set;
        nativeSetter.call(input, value);
        input.dispatchEvent(new Event('input', { bubbles: true }));
        input.dispatchEvent(new Event('change', { bubbles: true }));
    }

    /* ===============================
       LOCAL STORAGE HELPERS
    =============================== */

    function getStorageKey() { return (isBazaarPage() || isManageBazaarPage()) ? STORAGE_KEY_BAZAAR : STORAGE_KEY_MARKET; }

    let _savedItemsCache = {};
    function getSavedItems(key) {
        const k = key ?? getStorageKey();
        if (_savedItemsCache[k]) return _savedItemsCache[k];
        try { _savedItemsCache[k] = JSON.parse(localStorage.getItem(k)) || []; }
        catch { _savedItemsCache[k] = []; }
        return _savedItemsCache[k];
    }
    function saveItems(items, key) {
        const k = key ?? getStorageKey();
        _savedItemsCache[k] = items;
        localStorage.setItem(k, JSON.stringify(items));
    }
    function addItem(id) {
        const items = getSavedItems();
        if (!items.includes(id)) { items.push(id); saveItems(items); }
    }
    function removeItem(id) { saveItems(getSavedItems().filter(i => i !== id)); }
    function isItemSaved(id) { return getSavedItems().includes(id); }

    function getKeepQtyKey() { return (isBazaarPage() || isManageBazaarPage()) ? KEEP_QTY_KEY_BAZAAR : KEEP_QTY_KEY_MARKET; }
    let _keepQtyCache = {};
    function getKeepQtyMap() {
        const k = getKeepQtyKey();
        if (_keepQtyCache[k]) return _keepQtyCache[k];
        try { _keepQtyCache[k] = JSON.parse(localStorage.getItem(k)) || {}; }
        catch { _keepQtyCache[k] = {}; }
        return _keepQtyCache[k];
    }
    function getKeepQty(id) { return getKeepQtyMap()[id] ?? null; }
    function setKeepQty(id, value) {
        const map = getKeepQtyMap();
        if (value === null || value === '' || isNaN(value)) delete map[id];
        else map[id] = Number(value);
        _keepQtyCache[getKeepQtyKey()] = map;
        localStorage.setItem(getKeepQtyKey(), JSON.stringify(map));
    }
    function removeKeepQty(id) { setKeepQty(id, null); }

    let _itemMarginCache = null;
    function getItemMarginMap() {
        if (_itemMarginCache) return _itemMarginCache;
        try { _itemMarginCache = JSON.parse(localStorage.getItem(ITEM_MARGIN_KEY)) || {}; }
        catch { _itemMarginCache = {}; }
        return _itemMarginCache;
    }
    function getItemMargin(id) { return getItemMarginMap()[id] ?? null; }
    function setItemMargin(id, value) {
        const map = getItemMarginMap();
        if (value === null || value === '' || isNaN(value)) delete map[id];
        else map[id] = Number(value);
        _itemMarginCache = map;
        localStorage.setItem(ITEM_MARGIN_KEY, JSON.stringify(map));
    }
    function removeItemMargin(id) { setItemMargin(id, null); }

    function bustAllCaches() {
        _configCache = null; _fixedPriceCache = null; _priceCache = null;
        _lockedPricesCache = null; _savedItemsCache = {}; _keepQtyCache = {};
        _itemMarginCache = null; _playerIdCache = null;
    }

    /* ===============================
       IMPORT / EXPORT
    =============================== */

    function exportAll() {
        return JSON.stringify({
            market:      getSavedItems(STORAGE_KEY_MARKET),
            bazaar:      getSavedItems(STORAGE_KEY_BAZAAR),
            keepMarket:  JSON.parse(localStorage.getItem(KEEP_QTY_KEY_MARKET) || '{}'),
            keepBazaar:  JSON.parse(localStorage.getItem(KEEP_QTY_KEY_BAZAAR) || '{}'),
            itemMargin:  JSON.parse(localStorage.getItem(ITEM_MARGIN_KEY) || '{}'),
            fixedPrices: JSON.parse(localStorage.getItem(FIXED_PRICE_KEY) || '{}'),
        }, null, 2);
    }

    function importAll(json) {
        try {
            const d = JSON.parse(json);
            if (d.market)      saveItems(d.market, STORAGE_KEY_MARKET);
            if (d.bazaar)      saveItems(d.bazaar, STORAGE_KEY_BAZAAR);
            if (d.keepMarket)  localStorage.setItem(KEEP_QTY_KEY_MARKET, JSON.stringify(d.keepMarket));
            if (d.keepBazaar)  localStorage.setItem(KEEP_QTY_KEY_BAZAAR, JSON.stringify(d.keepBazaar));
            if (d.itemMargin)  localStorage.setItem(ITEM_MARGIN_KEY, JSON.stringify(d.itemMargin));
            if (d.fixedPrices) localStorage.setItem(FIXED_PRICE_KEY, JSON.stringify(d.fixedPrices));
            alert('Import OK');
            bustAllCaches();
        } catch {
            alert('Invalid JSON');
        }
    }

    /* ===============================
       ITEM HELPERS
    =============================== */

    function getItemId(row) {
        const img = row.querySelector('img.torn-item');
        if (!img) return null;
        const match = img.src.match(/items\/(\d+)\//);
        return match ? match[1] : null;
    }

    function getItemId_Bazaar(row) {
        const img = row.querySelector("img[src*='/images/items/']");
        if (!img) return null;
        const match = img.src.match(/items\/(\d+)\//);
        return match ? match[1] : null;
    }

    /* ===============================
       STYLES
    =============================== */

    function applyInputStyle(el) {
        Object.assign(el.style, {
            width: '42px', padding: '5px 3px',
            background: 'linear-gradient(0deg, rgb(17,17,17) 0%, rgb(0,0,0) 100%)',
            color: 'rgb(255,255,255)', textAlign: 'center',
            border: '0.8px solid rgb(68,68,68)', borderRadius: '5px',
            fontSize: '12px', fontFamily: 'Arial, Helvetica, sans-serif',
            lineHeight: '14px', verticalAlign: 'middle',
            appearance: 'none', MozAppearance: 'textfield', boxSizing: 'border-box',
        });
    }

    function ensureStyles() {
        if (document.getElementById('tm-keep-input-style')) return;
        const s = document.createElement('style');
        s.id = 'tm-keep-input-style';
        s.textContent = [
            'input.tm-keep-input::-webkit-outer-spin-button,',
            'input.tm-keep-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }',
            'input.tm-keep-input::placeholder { text-align: center; font-size: 10px; }',
            '.d .items-wrap .category-wrap ul.items-cont > li { position: relative; }',
            /* Manage Bazaar - widen price column in header and rows */
            '[data-testid="cells-header"] [class*="price"] { width: 256px !important; }',
            '[data-testid="sortable-item"] [class*="price"] { width: 260px !important; }',
            /* Manage Bazaar - fix price input padding */
            '.d .input-money-group input.input-money:not([type="file"]):not([type="submit"]):not([type="button"]) { padding: 5px 6px !important; }',
        ].join('\n');
        document.head.appendChild(s);
    }

    /* ===============================
       CONTROLS BUILDER
    =============================== */

    function buildControls(itemId, onAuto, onSave, onDelete, bazaar) {
        ensureStyles();

        const col = document.createElement('div');
        col.classList.add('tm-injected');
        col.style.cssText = 'display:flex;align-items:center;gap:4px;margin-left:6px;';

        if (bazaar) {
            Object.assign(col.style, {
                position: 'absolute', left: 'calc(100% - 6px)',
                background: '#333', height: '33px',
                border: '1px solid black', borderBottom: 'none',
                top: '-1px', paddingLeft: '3px',
            });
        }

        const fixedInput = document.createElement('input');
        fixedInput.type = 'text';
        fixedInput.placeholder = 'Fix $';
        fixedInput.title = 'Fixed price (overrides API/RRP)';
        fixedInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(fixedInput);
        fixedInput.style.display = 'none';

        const keepInput = document.createElement('input');
        keepInput.type = 'text';
        keepInput.placeholder = 'Keep';
        keepInput.title = 'Amount to keep after sale';
        keepInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(keepInput);
        keepInput.style.display = 'none';

        const marginInput = document.createElement('input');
        marginInput.type = 'text';
        marginInput.placeholder = '%';
        marginInput.title = 'Custom margin % for this item';
        marginInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(marginInput);
        marginInput.style.display = 'none';

        const btnA = document.createElement('button');
        btnA.textContent = 'A';
        btnA.title = 'Auto-price';
        btnA.style.cssText = 'padding:0 4px;cursor:pointer;color:#2196F3;';
        btnA.classList.add('tm-injected');
        btnA.addEventListener('click', e => { e.stopPropagation(); onAuto(); });

        const btnToggle = document.createElement('button');
        btnToggle.style.cssText = 'padding:0 4px;cursor:pointer;margin-right:5px;';
        btnToggle.classList.add('tm-injected');

        if (itemId) {
            const savedKeep = getKeepQty(itemId);
            if (savedKeep !== null) keepInput.value = savedKeep;
            const savedMargin = getItemMargin(itemId);
            if (savedMargin !== null) marginInput.value = savedMargin;
            const savedFixed = getFixedPrice(itemId);
            if (savedFixed !== null) fixedInput.value = savedFixed;
        }

        fixedInput.addEventListener('click', e => e.stopPropagation());
        fixedInput.addEventListener('change', () => {
            if (!itemId) return;
            setFixedPrice(itemId, fixedInput.value.trim() || null);
            onAuto();
        });

        keepInput.addEventListener('click', e => e.stopPropagation());
        keepInput.addEventListener('change', () => {
            if (!itemId) return;
            setKeepQty(itemId, keepInput.value.trim() || null);
            onAuto();
        });

        marginInput.addEventListener('click', e => e.stopPropagation());
        marginInput.addEventListener('change', () => {
            if (!itemId) return;
            setItemMargin(itemId, marginInput.value.trim() || null);
            onAuto();
        });

        function updateToggle() {
            if (!itemId) return;
            if (isItemSaved(itemId)) {
                btnToggle.textContent = 'D'; btnToggle.title = 'Remove';
                btnToggle.style.color = '#F44336';
                fixedInput.style.display = '';
                keepInput.style.display = '';
                marginInput.style.display = '';
            } else {
                btnToggle.textContent = 'S'; btnToggle.title = 'Save';
                btnToggle.style.color = '#4CAF50';
                fixedInput.style.display = 'none';
                keepInput.style.display = 'none';
                marginInput.style.display = 'none';
            }
        }

        btnToggle.addEventListener('click', e => {
            e.stopPropagation();
            if (!itemId) return;
            if (isItemSaved(itemId)) {
                removeItem(itemId); removeKeepQty(itemId); removeItemMargin(itemId);
                setFixedPrice(itemId, null);
                onDelete();
            } else {
                onSave(); addItem(itemId);
            }
            updateToggle();
        });

        updateToggle();

        col.appendChild(fixedInput);
        col.appendChild(keepInput);
        col.appendChild(marginInput);
        col.appendChild(btnA);
        col.appendChild(btnToggle);

        return col;
    }

    /* ===============================
       CONFIRM PAGE - net price summary
    =============================== */

    function getConfirmFeeRate() {
        const activeAnon = document.querySelector('[class*="anonymousPreview"][class*="active"]');
        const summaryText = document.querySelector('[class*="text"]')?.textContent ?? '';
        const hasAnonFee = activeAnon !== null || summaryText.includes('anonymity fee');
        return hasAnonFee ? 0.15 : 0.05;
    }

    function parseQty(titleEl) {
        const titleWrapper = titleEl?.closest('[class*="title"]');
        if (!titleWrapper) return 1;
        const spans = titleWrapper.querySelectorAll('span');
        for (const s of spans) {
            const m = s.textContent.trim().match(/^x(\d+)$/);
            if (m) return parseInt(m[1], 10);
        }
        return 1;
    }

    function injectConfirmNetPrices() {
        if (document.querySelector('.tm-confirm-injected')) return;

        const feeRate = getConfirmFeeRate();

        const rows = document.querySelectorAll('[class*="itemRowWrapper"]');
        let grandTotal = 0;

        rows.forEach(row => {
            if (row.querySelector('.tm-confirm-injected')) return;

            const priceEl   = row.querySelector('[class*="price___"]');
            const nameEl    = row.querySelector('[class*="name"]');
            const previewEl = row.querySelector('[class*="pricePreview"]');
            if (!priceEl) return;

            if (previewEl) {
                previewEl.style.flexDirection = 'column';
                previewEl.style.placeSelf     = 'flex-start';
                previewEl.style.alignItems    = 'flex-end';
            }

            const unitPrice = parseMoney(priceEl.textContent);
            const qty = parseQty(nameEl);
            const gross = unitPrice * qty;
            const net   = Math.round(gross * (1 - feeRate));
            grandTotal += net;

            const netEl = document.createElement('div');
            netEl.className = 'tm-confirm-injected tm-injected';
            netEl.textContent = `$${fmt(net)}`;
            netEl.title = `${qty} x $${fmt(unitPrice)} - ${feeRate * 100}% fee = $${fmt(net)}`;
            netEl.style.cssText = `
                font-size: 11px;
                color: #8BC34A;
                margin-top: 1px;
                text-align: right;
            `;

            priceEl.parentElement.appendChild(netEl);
        });

        injectGrandTotal(grandTotal, feeRate);
    }

    function injectGrandTotal(grandTotal, feeRate) {
        const textBlock = document.querySelector('[class*="text___"]');
        if (!textBlock || textBlock.querySelector('.tm-grand-total')) return;

        const totalEl = document.createElement('p');
        totalEl.className = 'tm-grand-total tm-injected';
        totalEl.style.cssText = `
            margin-top: 6px;
            font-size: 12px;
            color: #8BC34A;
            border-top: 1px solid #333;
            padding-top: 6px;
        `;
        totalEl.innerHTML = `Net to receive: <b style="color:#8BC34A;">$${fmt(grandTotal)}</b> <span style="color:#777;font-size:11px;">(after ${feeRate * 100}% fee)</span>`;
        textBlock.appendChild(totalEl);
    }

    function scanRows_Confirm() {
        injectConfirmNetPrices();
    }

    /* ===============================
       INIT & SCAN
    =============================== */

    function addButton_Market(row) {
        const info = getInfoEl(row);
        if (!info || info.dataset.tmButtonAdded) return;
        info.dataset.tmButtonAdded = '1';

        const priceWrapper = getPriceWrapper(row);
        if (priceWrapper) {
            priceWrapper.dataset.tmOrigWidth = priceWrapper.style.width || '';
            priceWrapper.style.width = '';
        }

        const itemId = getItemId(row);
        const col = buildControls(
            itemId,
            () => setValue_Market(row),
            () => setValue_Market(row),
            () => {},
            false
        );
        info.appendChild(col);
    }

    function addButton_Bazaar(row) {
        const actionsWrap = row.querySelector('.actions-main-wrap');
        if (!actionsWrap || actionsWrap.dataset.tmButtonAdded) return;
        actionsWrap.dataset.tmButtonAdded = '1';

        const itemId = getItemId_Bazaar(row);
        const col = buildControls(
            itemId,
            () => setValue_Bazaar(row),
            () => setValue_Bazaar(row),
            () => {},
            true
        );
        actionsWrap.appendChild(col);
    }

    function autoApplyForSaved_Market(row) {
        const itemId = getItemId(row);
        if (!itemId || !isItemSaved(itemId) || row.dataset.tmAutoApplied) return;
        row.dataset.tmAutoApplied = '1';
        setValue_Market(row);
    }

    function autoApplyForSaved_Bazaar(row) {
        const itemId = getItemId_Bazaar(row);
        if (!itemId || !isItemSaved(itemId) || row.dataset.tmAutoApplied) return;
        row.dataset.tmAutoApplied = '1';
        setValue_Bazaar(row);
    }

    function scanRows() {
        if (isManageBazaarPage())         scanRows_ManageBazaar();
        else if (isBazaarPage())          scanRows_Bazaar();
        else if (isViewListingPage())     scanRows_ViewListing();
        else if (isConfirmPage())         scanRows_Confirm();
        else                              scanRows_Market();
    }

    function cleanup() {
        document.querySelectorAll('.tm-injected').forEach(el => el.remove());
        document.querySelectorAll('[data-tm-button-added]').forEach(el => { delete el.dataset.tmButtonAdded; });
        document.querySelectorAll('[data-tm-view-added]').forEach(el => { delete el.dataset.tmViewAdded; });
        document.querySelectorAll('[data-tm-manage-added]').forEach(el => { delete el.dataset.tmManageAdded; });
        document.querySelectorAll('[class*="priceInputWrapper"][data-tm-orig-width]').forEach(wrapper => {
            wrapper.style.width = wrapper.dataset.tmOrigWidth;
            delete wrapper.dataset.tmOrigWidth;
        });
        document.querySelectorAll('[data-tm-auto-applied]').forEach(el => { delete el.dataset.tmAutoApplied; });
        document.getElementById('tm-keep-input-style')?.remove();
        document.getElementById('tm_settings')?.remove();
        document.getElementById('tm_settings_panel')?.remove();
        document.getElementById('tm_refresh_all_btn')?.remove();
        document.getElementById('tm_update_notification')?.remove();
    }

    /* ===============================
       REFRESH ALL WIDGET (viewListing)
    =============================== */

    function createRefreshAllWidget() {
        if (document.getElementById('tm_refresh_all_btn')) return;

        const btn = document.createElement('div');
        btn.id = 'tm_refresh_all_btn';
        btn.title = 'Refresh all listing prices (API)';
        btn.style.cssText = `
            position:fixed;right:0;bottom:calc(20% + 45px);z-index:9999;cursor:pointer;
            background:linear-gradient(90deg, rgb(2,36,74), rgb(3,111,201));
            padding:8px;border-radius:6px;
        `;
        btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
          <polyline points="23 4 23 10 17 10"/>
          <polyline points="1 20 1 14 7 14"/>
          <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
        </svg>`;

        btn.addEventListener('click', async () => {
            btn.style.opacity = '0.5';
            btn.style.pointerEvents = 'none';

            const rows = getViewListingRows();
            rows.forEach(row => {
                const itemId = getItemId(row);
                if (itemId && !isPriceLocked(itemId)) clearCacheEntry(itemId);
            });
            for (const row of rows) {
                const itemId = getItemId(row);
                if (itemId && isPriceLocked(itemId)) continue;
                await setValue_Market(row, true);
                refreshNameTitle(row);
            }

            btn.style.opacity = '1';
            btn.style.pointerEvents = '';
            updateViewListingSummary();
        });

        document.body.appendChild(btn);
    }

    function getViewListingRows() {
        const rows = [];
        document.querySelectorAll('button[aria-label^="View"], button[aria-label^="Expand"]')
            .forEach(button => {
                const row = button.closest('div');
                if (!row) return;
                const wrapper = row.parentElement;
                if (wrapper) rows.push(wrapper);
            });
        return rows;
    }

    function createWidget() {
        const btn = document.createElement('div');
        btn.id = 'tm_settings';
        btn.title = 'Junk Seller Settings';
        btn.style.cssText = `
            position:fixed;right:0;bottom:20%;z-index:9999;cursor:pointer;
            background:linear-gradient(90deg,#4a0202,#c90319);
            padding:8px;border-radius:6px;
        `;
        btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<g fill="white"><g transform="scale(1.5)" style="transform-origin:12px center;">
<path d="M1443,19.674a8.407,8.407,0,0,0-.659-1.593,2.576,2.576,0,0,1-2.582-.84,2.492,2.492,0,0,1-.65-2.582,8.4,8.4,0,0,0-1.592-.659,2.828,2.828,0,0,1-5.034,0,8.4,8.4,0,0,0-1.592.659,2.492,2.492,0,0,1-.65,2.582,2.578,2.578,0,0,1-2.582.841,8.371,8.371,0,0,0-.659,1.592A2.722,2.722,0,0,1,1428.539,22,3.015,3.015,0,0,1,1427,24.517a8.4,8.4,0,0,0,.659,1.592,2.595,2.595,0,0,1,3.232,3.232,8.338,8.338,0,0,0,1.592.659,2.828,2.828,0,0,1,5.034,0,8.4,8.4,0,0,0,1.592-.659,2.492,2.492,0,0,1-.65-2.582,2.576,2.576,0,0,1,2.582-.84,8.407,8.407,0,0,0,.659-1.593A2.722,2.722,0,0,1,1441.461,22,2.722,2.722,0,0,1,1443,19.674Zm-8,5.805A3.479,3.479,0,1,1,1438.479,22,3.48,3.48,0,0,1,1435,25.479Z" transform="translate(-1423 -10)"/>
</g></g></svg>`;

        const panel = document.createElement('div');
        panel.id = 'tm_settings_panel';
        const cfg = getConfig();
        panel.style.cssText = `
            position:fixed;right:50px;bottom:20%;background:#222;color:white;
            padding:0;display:none;z-index:9999;font-size:12px;border-radius:6px;
            min-width:240px;font-family:Arial,sans-serif;border:1px solid #444;
        `;

        panel.innerHTML = `
<div style="display:flex;border-bottom:1px solid #444;">
  <button class="tm-tab active" data-tab="pricing" style="${tabBtnStyle(true)}">Pricing</button>
  <button class="tm-tab" data-tab="io" style="${tabBtnStyle(false)}">Import/Export</button>
  <button class="tm-tab" data-tab="api" style="${tabBtnStyle(false)}">API</button>
</div>

<div class="tm-tab-content" data-content="pricing" style="padding:10px;display:block;">
  <div style="margin-bottom:8px;">
    <label>Margin %</label><br>
    <input id="tm_margin" type="text" value="${cfg.margin}"
      style="width:80px;padding:3px;margin-top:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
  </div>
  <div style="margin-bottom:8px;">
    <label>Rounding</label><br>
    <input id="tm_rounding" type="text" value="${cfg.rounding}"
      style="width:80px;padding:3px;margin-top:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
  </div>
  <div style="margin-bottom:8px;">
    <label>API cache (minutes)</label><br>
    <input id="tm_cache_ttl" type="text" value="${cfg.cacheTtlMin}"
      style="width:80px;padding:3px;margin-top:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
  </div>
  <div style="margin-bottom:8px;display:flex;align-items:center;gap:6px;">
    <input id="tm_bulk_detection" type="checkbox" ${cfg.bulkDetection !== false ? 'checked' : ''}
      style="cursor:pointer;width:14px;height:14px;accent-color:#2196F3;">
    <label for="tm_bulk_detection" style="cursor:pointer;">
      Bulk seller detection
      <span style="display:block;font-size:10px;color:#aaa;margin-top:1px;">
        Apply -10% discount when a seller has many items
      </span>
    </label>
  </div>
  <div style="margin-bottom:12px;">
    <label>Bulk seller threshold (items)</label><br>
    <input id="tm_bulk_threshold" type="text" value="${cfg.bulkThreshold ?? 200}"
      style="width:80px;padding:3px;margin-top:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
    <span style="display:block;font-size:10px;color:#aaa;margin-top:3px;">
      Min. quantity listed to count as bulk seller
    </span>
  </div>
  <button id="tm_save_cfg" style="${saveBtnStyle()}">Save</button>
</div>

<div class="tm-tab-content" data-content="io" style="padding:10px;display:none;">
  <div style="margin-bottom:6px;color:#aaa;font-size:11px;">
    Exports and imports item lists and per-item Keep &amp; Margin settings.
  </div>
  <textarea id="tm_io_textarea"
    style="width:100%;height:80px;resize:vertical;box-sizing:border-box;
           background:#111;color:white;border:1px solid #555;border-radius:3px;
           font-size:11px;padding:4px;"></textarea>
  <div style="display:flex;gap:6px;margin-top:6px;">
    <button id="tm_export_all" style="${saveBtnStyle()}">Export</button>
    <button id="tm_import_all" style="${saveBtnStyle()}">Import</button>
  </div>
</div>

<div class="tm-tab-content" data-content="api" style="padding:10px;display:none;">
  <div style="margin-bottom:6px;font-size:11px;color:#aaa;line-height:1.5;">
    Requires a <b style="color:white;">Public Only</b> key.<br>
    Get it from: <a href="https://www.torn.com/preferences.php#tab=api"
      target="_blank" style="color:#2196F3;">Settings &rarr; API Keys</a><br>
    Without a key, prices fall back to RRP from the table.
  </div>
  <div style="display:flex;align-items:center;gap:4px;margin-bottom:12px;">
    <input id="tm_api_key" type="password" value="${cfg.apiKey ?? ''}"
      placeholder="Public Only key"
      style="flex:1;padding:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
    <button id="tm_toggle_api_key" title="Show/hide key"
      style="padding:2px 6px;background:#333;color:white;border:1px solid #555;border-radius:3px;cursor:pointer;">&#128065;</button>
  </div>
  <button id="tm_save_api" style="${saveBtnStyle()}">Save</button>
</div>
`;

        panel.querySelectorAll('.tm-tab').forEach(tab => {
            tab.addEventListener('click', () => {
                panel.querySelectorAll('.tm-tab').forEach(t => {
                    t.style.background = '#333'; t.style.borderBottom = '1px solid #444';
                });
                panel.querySelectorAll('.tm-tab-content').forEach(c => c.style.display = 'none');
                tab.style.background = '#222'; tab.style.borderBottom = '1px solid #222';
                panel.querySelector(`.tm-tab-content[data-content="${tab.dataset.tab}"]`).style.display = 'block';
            });
        });

        btn.onclick = () => {
            panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
        };

        document.body.appendChild(btn);
        document.body.appendChild(panel);

        panel.querySelector('#tm_save_cfg').onclick = () => {
            const c = getConfig();
            c.margin         = parseFloat(panel.querySelector('#tm_margin').value) || 0;
            c.rounding       = parseInt(panel.querySelector('#tm_rounding').value) || 1;
            c.cacheTtlMin    = parseInt(panel.querySelector('#tm_cache_ttl').value) || 5;
            c.bulkDetection  = panel.querySelector('#tm_bulk_detection').checked;
            c.bulkThreshold  = parseInt(panel.querySelector('#tm_bulk_threshold').value) || 200;
            saveConfig(c);
            alert('Saved');
        };

        panel.querySelector('#tm_save_api').onclick = () => {
            const c = getConfig();
            c.apiKey = panel.querySelector('#tm_api_key').value.trim();
            saveConfig(c);
            alert('Saved');
        };

        panel.querySelector('#tm_toggle_api_key').onclick = () => {
            const inp = panel.querySelector('#tm_api_key');
            inp.type = inp.type === 'password' ? 'text' : 'password';
        };

        panel.querySelector('#tm_export_all').onclick = () => {
            panel.querySelector('#tm_io_textarea').value = exportAll();
        };
        panel.querySelector('#tm_import_all').onclick = () => {
            importAll(panel.querySelector('#tm_io_textarea').value);
        };
    }

    function tabBtnStyle(active) {
        return `flex:1;padding:6px 4px;cursor:pointer;font-size:11px;border:none;border-radius:0;` +
               `background:${active ? '#222' : '#333'};color:white;` +
               `border-bottom:${active ? '1px solid #222' : '1px solid #444'};`;
    }

    function saveBtnStyle() {
        return 'padding:3px 8px;cursor:pointer;color:white;background:#444;border:1px solid #666;border-radius:3px;';
    }

    /* ===============================
       SCAN
    =============================== */

    function scanRows_Market() {
        document.querySelectorAll('button[aria-label^="View"], button[aria-label^="Expand"]')
            .forEach(button => {
                const row = button.closest('div');
                if (!row) return;
                const wrapper = row.parentElement;
                if (!wrapper) return;
                addButton_Market(wrapper);
                autoApplyForSaved_Market(wrapper);
            });
    }

    function scanRows_ViewListing() {
        document.querySelectorAll('button[aria-label^="View"], button[aria-label^="Expand"]')
            .forEach(button => {
                const row = button.closest('div');
                if (!row) return;
                const wrapper = row.parentElement;
                if (!wrapper) return;
                addButton_ViewListing(wrapper);
            });
    }

    function updateViewListingSummary() {
        const selectAllEl = document.querySelector('[class*="selectAll"]');
        if (!selectAllEl) return;

        let totalNet   = 0;
        let totalGross = 0;
        let itemCount  = 0;

        document.querySelectorAll('[class*="itemRowWrapper"]').forEach(row => {
            const priceHidden = row.querySelector('input[data-money="Infinity"][type="hidden"]');
            const qtyHidden   = row.querySelector('input[type="hidden"]:not([data-money="Infinity"])');
            if (!priceHidden || !qtyHidden) return;

            const price = Number(priceHidden.value);
            const qty   = Number(qtyHidden.value);
            if (!price || !qty) return;

            const isAnon  = !!row.querySelector('[class*="checkboxWrapper"][class*="active"]');
            const feeRate = isAnon ? 0.15 : 0.05;
            const gross   = price * qty;
            const net     = Math.round(gross * (1 - feeRate));

            totalGross += gross;
            totalNet   += net;
            itemCount  += qty;
        });

        let summaryEl = document.getElementById('tm_view_summary');
        if (!summaryEl) {
            summaryEl = document.createElement('div');
            summaryEl.id = 'tm_view_summary';
            summaryEl.classList.add('tm-injected');
            summaryEl.style.cssText = `
                padding: 6px 10px;
                font-size: 12px;
                font-family: Arial, Helvetica, sans-serif;
                color: #aaa;
                border-top: 1px solid #333;
                margin-top: 4px;
                line-height: 1.7;
            `;
            selectAllEl.insertAdjacentElement('afterend', summaryEl);
        }

        const fees = totalGross - totalNet;
        const tooltip = `Listed: ${fmt(itemCount)} | Gross: $${fmt(totalGross)} | Fees: -$${fmt(fees)} | Net: $${fmt(totalNet)}`;
        summaryEl.title = tooltip;
        summaryEl.innerHTML = `
            <span style="color:#777;">Listed:</span>
            <b style="color:white;">${fmt(itemCount)}</b>
            <span style="color:#777;margin-left:8px;">Gross:</span>
            <b style="color:white;">$${fmt(totalGross)}</b>
            <span style="color:#777;margin-left:8px;">Fees:</span>
            <b style="color:#F44336;">-$${fmt(fees)}</b>
            <span style="color:#777;margin-left:8px;">Net:</span>
            <b style="color:#8BC34A;">$${fmt(totalNet)}</b>
        `;
    }

    function scanRows_Bazaar() {
        document.querySelectorAll('li[data-group="child"]').forEach(row => {
            addButton_Bazaar(row);
            autoApplyForSaved_Bazaar(row);
        });
    }

    let initialized = false;
    let mutationObserver = null;
    let _scanDebounceTimer = null;

    function initScript() {
        if (!isActivePage() || initialized) return;
        initialized = true;

        mutationObserver = new MutationObserver(() => {
            clearTimeout(_scanDebounceTimer);
            _scanDebounceTimer = setTimeout(() => {
                scanRows();
                if (isViewListingPage()) {
                    createRefreshAllWidget();
                    if (!document.getElementById('tm_view_summary')) updateViewListingSummary();
                }
                if (isManageBazaarPage()) {
                    createRefreshAllWidget_ManageBazaar();
                }
            }, 80);
        });
        mutationObserver.observe(document.body, { childList: true, subtree: true });

        scanRows();
        if (!isConfirmPage()) createWidget();
        if (isViewListingPage()) {
            createRefreshAllWidget();
            updateViewListingSummary();
        }
        if (isManageBazaarPage()) {
            createRefreshAllWidget_ManageBazaar();
        }

        checkVersion();
    }

    function deactivate() {
        clearTimeout(_scanDebounceTimer);
        mutationObserver?.disconnect();
        mutationObserver = null;
        cleanup();
        initialized = false;
    }

    initScript();
    window.addEventListener('hashchange', () => {
        deactivate();
        if (isActivePage()) initScript();
    });

})();