Torn — Sort items by value

Sort items by total or single value

Verze ze dne 10. 08. 2025. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         Torn — Sort items by value
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Sort items by total or single value
// @author       Charkel [3429133]
// @match        https://www.torn.com/item.php*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const TITLE_BAR_SELECTOR = '.title-black.hospital-dark.top-round.scroll-dark';
    const BUTTON_CONTAINER_CLASS = 'tm-sort-buttons';
    let itemsFullyLoaded = false;
    let loadingOverlay;

    // Inject CSS for buttons and overlay
    const style = document.createElement('style');
    style.textContent = `
        .tm-sort-btn {
            margin-left: 6px;
            padding: 3px 7px;
            background: #acea00;
            border: 1px solid #222;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
            font-weight: 700;
        }
        .tm-loading-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100vw;
            height: 100vh;
            background: rgba(0,0,0,0.70);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 9999;
            font-size: 48px;
            font-weight: bold;
            color: #fff;
            text-shadow:
                -2px -2px 4px rgba(0,0,0,0.9),
                 2px -2px 4px rgba(0,0,0,0.9),
                -2px  2px 4px rgba(0,0,0,0.9),
                 2px  2px 4px rgba(0,0,0,0.9);
            pointer-events: none;
        }
    `;
    document.head.appendChild(style);

    function createButton(label, sortType) {
        const btn = document.createElement('button');
        btn.className = 'tm-sort-btn';
        btn.textContent = label + ' ↓';
        btn.dataset.order = 'desc';
        btn.dataset.sortType = sortType;
        return btn;
    }

    function showLoadingOverlay() {
        if (!loadingOverlay) {
            loadingOverlay = document.createElement('div');
            loadingOverlay.className = 'tm-loading-overlay';
            loadingOverlay.textContent = 'Loading items, please wait…';
            document.body.appendChild(loadingOverlay);
        }
        loadingOverlay.style.display = 'flex';
    }

    function hideLoadingOverlay() {
        if (loadingOverlay) loadingOverlay.style.display = 'none';
    }

    async function loadAllItems() {
        if (itemsFullyLoaded) return;

        showLoadingOverlay();

        return new Promise(resolve => {
            let lastHeight = 0;
            let sameHeightCount = 0;
            let attempts = 0;
            const maxAttempts = 100;

            function scrollStep() {
                attempts++;
                window.scrollTo(0, document.body.scrollHeight);
                const newHeight = document.body.scrollHeight;

                if (newHeight === lastHeight) {
                    sameHeightCount++;
                    if (sameHeightCount > 5 || attempts > maxAttempts) {
                        itemsFullyLoaded = true;
                        hideLoadingOverlay();
                        resolve();
                        return;
                    }
                } else {
                    sameHeightCount = 0;
                    lastHeight = newHeight;
                }
                setTimeout(scrollStep, 300);
            }
            scrollStep();
        });
    }

    function parsePriceElem(priceElem) {
        if (!priceElem) return { single: 0, total: 0, qty: 0 };
        const rawText = priceElem.textContent.replace(/\u00A0/g, ' ').trim();
        if (/N\/A/i.test(rawText)) return { single: 0, total: 0, qty: 0 };

        const numTokens = (rawText.match(/\d[\d,]*/g) || []).map(s => parseInt(s.replace(/,/g, ''), 10));
        let qty = 0;
        const qtySpan = priceElem.querySelector('.tt-item-quantity');
        const qtyMatch = qtySpan?.textContent.match(/(\d+)/) || rawText.match(/(\d+)\s*x/i);
        if (qtyMatch) qty = parseInt(qtyMatch[1], 10);

        if (qty) {
            const total = numTokens.length ? numTokens[numTokens.length - 1] : 0;
            let single = numTokens.find(n => n !== qty && n !== total) || 0;
            if (!single && total && qty) single = Math.round(total / qty);
            if (numTokens.length >= 1 && single && qty && numTokens[0] * qty === total) single = numTokens[0];
            return { single: single || 0, total: total || 0, qty };
        }

        if (numTokens.length === 0) return { single: 0, total: 0, qty: 0 };
        if (numTokens.length === 1) return { single: numTokens[0], total: numTokens[0], qty: 0 };

        const max = Math.max(...numTokens);
        const min = Math.min(...numTokens);
        if (max % min === 0 && (max / min) <= 1000) return { single: min, total: max, qty: Math.round(max / min) };
        return { single: numTokens[0], total: max, qty: 0 };
    }

    function isVisible(el) {
        return !!(el?.offsetWidth || el?.offsetHeight || el?.getClientRects().length);
    }

    function sortVisibleLists(sortType, order) {
        let containers = Array.from(document.querySelectorAll('.items-cont, .itemsList, ul.items-cont, ul.itemsList'))
            .filter(isVisible);

        if (!containers.length) {
            containers = Array.from(document.querySelectorAll('ul'))
                .filter(u => /-items$/.test(u.id) && isVisible(u));
        }

        containers.forEach(container => {
            const items = Array.from(container.children).filter(el => el.tagName === 'LI');
            if (!items.length) return;

            const pairs = items.map((li, i) => {
                const priceElem = li.querySelector('.tt-item-price');
                const parsed = parsePriceElem(priceElem);
                const val = sortType === 'single' ? parsed.single : parsed.total;
                return { li, val, index: i };
            });

            pairs.sort((a, b) => {
                if (a.val === b.val) return a.index - b.index;
                return order === 'desc' ? b.val - a.val : a.val - b.val;
            });

            pairs.forEach(p => container.appendChild(p.li));
        });
    }

    function addSortButtonsOnce() {
        const titleBar = document.querySelector(TITLE_BAR_SELECTOR);
        if (!titleBar || titleBar.querySelector('.' + BUTTON_CONTAINER_CLASS)) return;

        const container = document.createElement('div');
        container.className = BUTTON_CONTAINER_CLASS;
        container.style.display = 'inline-block';
        container.style.marginLeft = '8px';

        const btnTotal = createButton('Total Value', 'total');
        const btnSingle = createButton('Single Value', 'single');

        function clickHandler(clickedBtn, otherBtn) {
            return async function () {
                const orderToUse = clickedBtn.dataset.order || 'desc';
                otherBtn.dataset.order = 'desc';
                otherBtn.textContent = otherBtn.textContent.replace(/↓|↑/, '↓');

                await loadAllItems();
                sortVisibleLists(clickedBtn.dataset.sortType, orderToUse);

                clickedBtn.dataset.order = orderToUse === 'desc' ? 'asc' : 'desc';
                clickedBtn.textContent = clickedBtn.textContent.replace(/↓|↑/, clickedBtn.dataset.order === 'desc' ? '↓' : '↑');

                window.scrollTo(0, 0);
            };
        }

        btnTotal.addEventListener('click', clickHandler(btnTotal, btnSingle));
        btnSingle.addEventListener('click', clickHandler(btnSingle, btnTotal));

        container.appendChild(btnTotal);
        container.appendChild(btnSingle);
        titleBar.appendChild(container);
    }

    const observer = new MutationObserver(addSortButtonsOnce);
    const targetNode = document.querySelector('#mainContainer') || document.documentElement;
    observer.observe(targetNode, { childList: true, subtree: true });

    addSortButtonsOnce();
})();