Torn – Max Buy Button v5

MAX-Button im Item Market (sellerRow) und Bazaar (buy-menu)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn – Max Buy Button v5
// @namespace    https://www.torn.com/
// @version      5.0
// @description  MAX-Button im Item Market (sellerRow) und Bazaar (buy-menu)
// @author       Du
// @match        https://www.torn.com/bazaar.php*
// @match        https://www.torn.com/imarket.php*
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    document.head.insertAdjacentHTML('beforeend', `<style>
        .tmb-max {
            display: inline-flex !important;
            align-items: center !important;
            justify-content: center !important;
            padding: 0 7px !important;
            height: 22px !important;
            background: #c0392b !important;
            color: #fff !important;
            font-size: 10px !important;
            font-weight: 800 !important;
            font-family: inherit !important;
            border: none !important;
            border-radius: 3px !important;
            cursor: pointer !important;
            letter-spacing: 0.5px !important;
            margin-left: 4px !important;
            box-shadow: 0 1px 3px rgba(0,0,0,0.4) !important;
            vertical-align: middle !important;
            flex-shrink: 0 !important;
            white-space: nowrap !important;
            line-height: 1 !important;
            position: static !important;
        }
        .tmb-max:hover { background: #e74c3c !important; }
        .tmb-max.ok    { background: #27ae60 !important; }

        /* Bazaar: buy-menu soll flex sein damit MAX daneben passt */
        [data-testid="buy-menu"] {
            display: flex !important;
            align-items: center !important;
            flex-wrap: nowrap !important;
            gap: 4px !important;
        }
        [data-testid="buy-form"] {
            display: flex !important;
            align-items: center !important;
            gap: 4px !important;
        }
    </style>`);

    /* ── React-safe value setter ── */
    function setVal(inp, v) {
        try {
            const d = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
            if (d && d.set) d.set.call(inp, String(v));
            else inp.value = String(v);
        } catch(e) { inp.value = String(v); }
        inp.dispatchEvent(new Event('input',  { bubbles: true }));
        inp.dispatchEvent(new Event('change', { bubbles: true }));
    }

    function flash(btn) {
        btn.classList.add('ok');
        btn.textContent = '✓';
        setTimeout(() => { btn.classList.remove('ok'); btn.textContent = 'MAX'; }, 700);
    }

    /* ════════════════════════════════════════
       ITEM MARKET
       Echte Klassen laut Console-Output:
         Zeile:    sellerRow___PaRgK
         Buy-Link: linkWrap___oVXBN  (a-Tag, klickbar)
         Input:    irgendwo in buyControls / amountInput
    ════════════════════════════════════════ */
    function injectItemMarket(row) {
        if (row.dataset.tmbDone) return;

        // Der echte Buy-Link (linkWrap) – NUR den nehmen, nicht userInfoBox etc.
        const buyLink = row.querySelector('a[class*="linkWrap"]');
        if (!buyLink) return;

        // Schon injiziert?
        if (row.querySelector('.tmb-max')) return;
        row.dataset.tmbDone = '1';

        // "X available" aus Text der Zeile
        const txt = row.innerText || '';
        const m   = txt.match(/(\d+)\s+available/i);
        const maxQty = m ? parseInt(m[1]) : null;

        // Input suchen – in buyControls oder amountInput
        const input = row.querySelector(
            '[class*="amountInput"] input, ' +
            '[class*="buyControl"] input, ' +
            '.input-money-group input, ' +
            'input[type="number"], ' +
            'input[type="text"]'
        );

        const btn = document.createElement('button');
        btn.className   = 'tmb-max';
        btn.textContent = 'MAX';
        btn.type        = 'button';
        btn.title       = maxQty ? `Alle ${maxQty} kaufen` : 'Maximum kaufen';

        btn.addEventListener('click', e => {
            e.preventDefault();
            e.stopPropagation();

            if (input && maxQty) {
                setVal(input, maxQty);
                flash(btn);
                // Kurz warten damit React den neuen Wert verarbeitet, dann klicken
                setTimeout(() => buyLink.click(), 400);
            } else if (maxQty === 1 || !input) {
                // Nur 1 verfügbar oder kein Input → direkt kaufen
                flash(btn);
                setTimeout(() => buyLink.click(), 300);
            }
        });

        // Direkt neben den Buy-Link einfügen
        buyLink.insertAdjacentElement('afterend', btn);
    }

    /* ════════════════════════════════════════
       BAZAAR
       Container: data-testid="buy-menu"
       Input:     data-testid="number-input"
       Submit:    button[type="submit"] in buy-form
       Stock:     "(X in stock)" im item-container
    ════════════════════════════════════════ */
    function injectBazaar(menu) {
        if (menu.dataset.tmbDone) return;
        if (menu.querySelector('.tmb-max')) return;
        menu.dataset.tmbDone = '1';

        const input   = menu.querySelector('[data-testid="number-input"], input[type="text"], input[type="number"]');
        const buyForm = menu.querySelector('[data-testid="buy-form"]') || menu.closest('form');
        const buyBtn  = menu.querySelector('button[type="submit"]') ||
                        menu.querySelector('[data-testid="buy-button"]') ||
                        buyForm?.querySelector('button[type="submit"]');

        if (!input && !buyBtn) return;

        // Stock aus item-container lesen
        const itemEl = menu.closest('[data-testid="item"], [class*="item__"]') || menu.parentElement;
        const txt    = itemEl?.innerText || '';
        const m      = txt.match(/\((\d+)\s+in\s+stock\)/i) || txt.match(/(\d+)\s+in\s+stock/i);
        const stock  = m ? parseInt(m[1]) : parseInt(input?.max || '1');

        const btn = document.createElement('button');
        btn.className   = 'tmb-max';
        btn.textContent = 'MAX';
        btn.type        = 'button';
        btn.title       = `Alle ${stock} kaufen`;

        btn.addEventListener('click', e => {
            e.preventDefault();
            e.stopPropagation();
            if (input) setVal(input, stock);
            flash(btn);
            setTimeout(() => {
                if (buyBtn) buyBtn.click();
                else if (buyForm) {
                    buyForm.requestSubmit ? buyForm.requestSubmit() : buyForm.submit();
                }
            }, 400);
        });

        // Neben Buy-Button einfügen (direkt im buy-form oder buy-menu)
        if (buyBtn) {
            buyBtn.insertAdjacentElement('afterend', btn);
        } else if (buyForm) {
            buyForm.appendChild(btn);
        } else {
            menu.appendChild(btn);
        }
    }

    /* ════════════════════════════════════════
       SCAN
    ════════════════════════════════════════ */
    function scan() {
        // Item Market: NUR sellerRow Zeilen – nicht rowWrapper/userInfoBox
        document.querySelectorAll('[class*="sellerRow"]:not([data-tmb-done])').forEach(injectItemMarket);

        // Bazaar: buy-menu container
        document.querySelectorAll('[data-testid="buy-menu"]:not([data-tmb-done])').forEach(injectBazaar);
    }

    let t;
    new MutationObserver(() => { clearTimeout(t); t = setTimeout(scan, 150); })
        .observe(document.body, { childList: true, subtree: true });

    setTimeout(scan, 700);
    setTimeout(scan, 2000);

})();