Torn — Sort items by value

Sort items by total or single value

Versione datata 10/08/2025. Vedi la nuova versione l'ultima versione.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==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();
})();