Torn User List Stats

Display health and last online status for users in any user list

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         Torn User List Stats
// @namespace    https://torn.com/
// @version      2.1
// @description  Display health and last online status for users in any user list
// @author       You
// @match        https://www.torn.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// ==/UserScript==

(function() {
    'use strict';

    const STORAGE_KEYS = {
        API_KEY: 'torn_api_key',
        SHOW_USER_LISTS: 'torn_show_user_lists',
        SHOW_TRAVEL_LISTS: 'torn_show_travel_lists'
    };

    const FETCH_DELAY = 150;

    let apiKey = null;
    let showUserLists = true;
    let showTravelLists = true;
    let fetchQueue = [];
    let isFetching = false;
    let processedUsers = new Set();
    let settingsInjected = false;

    function loadSettings() {
        apiKey = GM_getValue(STORAGE_KEYS.API_KEY, null);
        showUserLists = GM_getValue(STORAGE_KEYS.SHOW_USER_LISTS, true);
        showTravelLists = GM_getValue(STORAGE_KEYS.SHOW_TRAVEL_LISTS, true);
    }

    function saveSettings(newApiKey, newShowUserLists, newShowTravelLists) {
        if (newApiKey !== undefined) {
            GM_setValue(STORAGE_KEYS.API_KEY, newApiKey);
            apiKey = newApiKey;
        }
        if (newShowUserLists !== undefined) {
            GM_setValue(STORAGE_KEYS.SHOW_USER_LISTS, newShowUserLists);
            showUserLists = newShowUserLists;
        }
        if (newShowTravelLists !== undefined) {
            GM_setValue(STORAGE_KEYS.SHOW_TRAVEL_LISTS, newShowTravelLists);
            showTravelLists = newShowTravelLists;
        }
    }

    function maskApiKey(key) {
        if (!key || key.length < 8) return key || '';
        return '••••••••' + key.slice(-6);
    }

    function createModal() {
        const existingModal = document.getElementById('tuls-settings-modal');
        if (existingModal) {
            existingModal.style.display = 'flex';
            return;
        }

        const overlay = document.createElement('div');
        overlay.id = 'tuls-settings-modal';
        overlay.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.7);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 999999;
        `;

        const modal = document.createElement('div');
        modal.style.cssText = `
            background: #1a1a1a;
            border: 1px solid #333;
            border-radius: 5px;
            padding: 20px;
            min-width: 320px;
            max-width: 400px;
            color: #ccc;
            font-family: Arial, sans-serif;
        `;

        const title = document.createElement('h3');
        title.textContent = 'User List Stats Settings';
        title.style.cssText = `
            margin: 0 0 20px 0;
            padding: 0 0 10px 0;
            border-bottom: 1px solid #333;
            color: #fff;
            font-size: 16px;
        `;
        modal.appendChild(title);

        const apiSection = document.createElement('div');
        apiSection.style.cssText = 'margin-bottom: 20px;';

        const apiLabel = document.createElement('label');
        apiLabel.textContent = 'API Key';
        apiLabel.style.cssText = 'display: block; margin-bottom: 8px; font-size: 13px;';
        apiSection.appendChild(apiLabel);

        const apiInputWrapper = document.createElement('div');
        apiInputWrapper.style.cssText = 'display: flex; gap: 8px;';

        const apiInput = document.createElement('input');
        apiInput.type = 'text';
        apiInput.id = 'tuls-api-key-input';
        apiInput.placeholder = 'Enter API key...';
        apiInput.value = apiKey ? maskApiKey(apiKey) : '';
        apiInput.style.cssText = `
            flex: 1;
            padding: 8px 10px;
            background: #2a2a2a;
            border: 1px solid #444;
            border-radius: 3px;
            color: #fff;
            font-size: 13px;
        `;

        let isEditing = !apiKey;
        apiInput.addEventListener('focus', function() {
            if (!isEditing) {
                this.value = '';
                this.placeholder = 'Enter new API key...';
                isEditing = true;
            }
        });

        apiInput.addEventListener('blur', function() {
            if (isEditing && this.value === '') {
                if (apiKey) {
                    this.value = maskApiKey(apiKey);
                    this.placeholder = 'Enter API key...';
                    isEditing = false;
                }
            }
        });

        apiInputWrapper.appendChild(apiInput);
        apiSection.appendChild(apiInputWrapper);
        modal.appendChild(apiSection);

        const togglesSection = document.createElement('div');
        togglesSection.style.cssText = 'margin-bottom: 20px;';

        function createToggle(id, label, checked) {
            const container = document.createElement('div');
            container.style.cssText = `
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 10px 0;
                border-bottom: 1px solid #333;
            `;

            const labelEl = document.createElement('span');
            labelEl.textContent = label;
            labelEl.style.cssText = 'font-size: 13px;';
            container.appendChild(labelEl);

            const toggleWrapper = document.createElement('div');
            toggleWrapper.style.cssText = 'position: relative;';

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.id = id;
            checkbox.checked = checked;
            checkbox.style.cssText = `
                width: 40px;
                height: 20px;
                appearance: none;
                background: #444;
                border-radius: 10px;
                cursor: pointer;
                position: relative;
                transition: background 0.2s;
            `;

            checkbox.addEventListener('change', function() {
                this.style.background = this.checked ? '#7ca900' : '#444';
            });

            if (checked) {
                checkbox.style.background = '#7ca900';
            }

            const style = document.createElement('style');
            style.textContent = `
                #${id}::before {
                    content: '';
                    position: absolute;
                    width: 16px;
                    height: 16px;
                    background: #fff;
                    border-radius: 50%;
                    top: 2px;
                    left: 2px;
                    transition: transform 0.2s;
                }
                #${id}:checked::before {
                    transform: translateX(20px);
                }
            `;
            document.head.appendChild(style);

            toggleWrapper.appendChild(checkbox);
            container.appendChild(toggleWrapper);

            return container;
        }

        togglesSection.appendChild(createToggle('tuls-toggle-user-lists', 'Show on user lists', showUserLists));
        togglesSection.appendChild(createToggle('tuls-toggle-travel-lists', 'Show on travel lists', showTravelLists));
        modal.appendChild(togglesSection);

        const buttonSection = document.createElement('div');
        buttonSection.style.cssText = 'display: flex; gap: 10px; justify-content: flex-end;';

        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = 'Cancel';
        cancelBtn.style.cssText = `
            padding: 8px 16px;
            background: #333;
            border: 1px solid #444;
            border-radius: 3px;
            color: #ccc;
            cursor: pointer;
            font-size: 13px;
        `;
        cancelBtn.addEventListener('click', function() {
            overlay.style.display = 'none';
        });
        buttonSection.appendChild(cancelBtn);

        const saveBtn = document.createElement('button');
        saveBtn.textContent = 'Save';
        saveBtn.style.cssText = `
            padding: 8px 16px;
            background: #7ca900;
            border: none;
            border-radius: 3px;
            color: #fff;
            cursor: pointer;
            font-size: 13px;
        `;
        saveBtn.addEventListener('click', function() {
            const inputVal = apiInput.value.trim();
            const newUserLists = document.getElementById('tuls-toggle-user-lists').checked;
            const newTravelLists = document.getElementById('tuls-toggle-travel-lists').checked;

            if (isEditing && inputVal && !inputVal.startsWith('••••')) {
                saveSettings(inputVal, newUserLists, newTravelLists);
                apiInput.value = maskApiKey(inputVal);
                isEditing = false;
            } else {
                saveSettings(undefined, newUserLists, newTravelLists);
            }

            overlay.style.display = 'none';
            processedUsers.clear();
            setTimeout(scanForUsers, 100);
        });
        buttonSection.appendChild(saveBtn);

        modal.appendChild(buttonSection);
        overlay.appendChild(modal);

        overlay.addEventListener('click', function(e) {
            if (e.target === overlay) {
                overlay.style.display = 'none';
            }
        });

        document.body.appendChild(overlay);
    }

    function injectSettingsMenuItem() {
        if (settingsInjected) return;

        const settingsMenu = document.querySelector('ul.settings-menu');
        if (!settingsMenu) return;

        const existingItem = document.getElementById('tuls-settings-menu-item');
        if (existingItem) return;

        const menuItem = document.createElement('li');
        menuItem.id = 'tuls-settings-menu-item';
        menuItem.className = 'link';
        menuItem.innerHTML = `
            <a href="#" style="display: flex; align-items: center;">
                <div class="icon-wrapper">
                    <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="-6 -4 28 28">
                        <path d="M8,0C3.58,0,0,3.58,0,8s3.58,8,8,8,8-3.58,8-8S12.42,0,8,0Zm0,14c-3.31,0-6-2.69-6-6s2.69-6,6-6,6,2.69,6,6-2.69,6-6,6Zm0-10c-2.21,0-4,1.79-4,4s1.79,4,4,4,4-1.79,4-4-1.79-4-4-4Zm0,6c-1.1,0-2-.9-2-2s.9-2,2-2,2,.9,2,2-.9,2-2,2Z"/>
                    </svg>
                </div>
                <span class="link-text">User List Stats</span>
            </a>
        `;

        menuItem.querySelector('a').addEventListener('click', function(e) {
            e.preventDefault();
            e.stopPropagation();
            createModal();
        });

        const logoutItem = settingsMenu.querySelector('li.link a[href*="logout"]')?.parentElement;
        if (logoutItem) {
            settingsMenu.insertBefore(menuItem, logoutItem);
        } else {
            settingsMenu.appendChild(menuItem);
        }

        settingsInjected = true;
    }

    function fetchUserData(userId) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://api.torn.com/user/${userId}?selections=profile&key=${apiKey}`,
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.error) {
                            reject(data.error);
                        } else {
                            resolve(data);
                        }
                    } catch (e) {
                        reject(e);
                    }
                },
                onerror: function(error) {
                    reject(error);
                }
            });
        });
    }

    function formatLastAction(timestamp) {
        if (!timestamp) return '?';

        const now = Math.floor(Date.now() / 1000);
        const diff = now - timestamp;

        if (diff < 60) return `${diff}s ago`;
        if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
        if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
        return `${Math.floor(diff / 86400)}d ago`;
    }

    function formatHealth(current, max) {
        if (current === undefined || max === undefined) return '?';
        return `${current.toLocaleString()}/${max.toLocaleString()}`;
    }

    function createStatsCell(health, lastAction) {
        const cell = document.createElement('div');
        cell.className = 'torn-userlist-stats';
        cell.style.cssText = 'font-size: 12px; padding: 8px 10px; display: flex; flex-direction: column; justify-content: center; min-width: 100px;';

        const healthColor = health.current === health.max ? '#7ca900' :
                           health.current > health.max * 0.5 ? '#e6a400' : '#c90000';

        cell.innerHTML = `
            <span style="color: ${healthColor};">${formatHealth(health.current, health.max)}</span>
            <span style="color: #999; font-size: 11px;">${formatLastAction(lastAction)}</span>
        `;

        return cell;
    }

    function addColumnHeader() {
        if (document.querySelector('.torn-userlist-stats-header')) return;

        const tableHead = document.querySelector('[class*="tableHead___"]');
        if (!tableHead) return;

        const levelHeader = tableHead.querySelector('[class*="level___"]');
        if (!levelHeader) return;

        const headerDiv = document.createElement('div');
        headerDiv.className = 'torn-userlist-stats-header';
        headerDiv.style.cssText = 'padding: 8px 10px; font-weight: bold; min-width: 100px; display: flex; align-items: center;';
        headerDiv.textContent = 'HP / Activity';

        levelHeader.insertAdjacentElement('afterend', headerDiv);
    }

    function injectStats(userRow, data, listType) {
        const existing = userRow.querySelector('.torn-userlist-stats');
        if (existing) existing.remove();

        const health = {
            current: data.life?.current,
            max: data.life?.maximum
        };
        const lastAction = data.last_action?.timestamp;

        if (listType === 'new') {
            addColumnHeader();

            const levelDiv = userRow.querySelector('[class*="level___"]');
            if (levelDiv) {
                const statsCell = createStatsCell(health, lastAction);
                levelDiv.insertAdjacentElement('afterend', statsCell);
            }
        } else {
            const levelSpan = userRow.querySelector('.left-side .level');
            if (levelSpan) {
                const span = document.createElement('span');
                span.className = 'torn-userlist-stats';
                span.style.cssText = 'margin-left: 8px; font-size: 11px; color: #999;';

                const healthColor = health.current === health.max ? '#7ca900' :
                                   health.current > health.max * 0.5 ? '#e6a400' : '#c90000';

                span.innerHTML = `<span style="color: ${healthColor};">HP: ${formatHealth(health.current, health.max)}</span> | <span>${formatLastAction(lastAction)}</span>`;
                levelSpan.insertAdjacentElement('afterend', span);
            }
        }
    }

    async function processQueue() {
        if (isFetching || fetchQueue.length === 0 || !apiKey) return;

        isFetching = true;

        while (fetchQueue.length > 0) {
            const { userId, element, listType } = fetchQueue.shift();

            try {
                const data = await fetchUserData(userId);
                injectStats(element, data, listType);
            } catch (error) {
                console.error(`Failed to fetch data for user ${userId}:`, error);

                if (error.code === 2) {
                    console.log('Torn User List Stats: Invalid API key');
                    fetchQueue = [];
                    break;
                }
            }

            if (fetchQueue.length > 0) {
                await new Promise(r => setTimeout(r, FETCH_DELAY));
            }
        }

        isFetching = false;
    }

    function extractUserId(element) {
        const selectors = [
            'a[href*="profiles.php?XID="]',
            'a.user.name[href*="XID="]'
        ];

        for (const selector of selectors) {
            const link = element.querySelector(selector);
            if (link) {
                const match = link.href.match(/XID=(\d+)/);
                if (match) return match[1];
            }
        }
        return null;
    }

    function isTravelPage() {
        return window.location.pathname.includes('/abroad') ||
               window.location.pathname.includes('/travel') ||
               document.querySelector('.travel-agency') !== null ||
               document.querySelector('[class*="travel"]') !== null;
    }

    function scanForUsers() {
        if (!apiKey) return;

        const onTravelPage = isTravelPage();

        if (showUserLists && !onTravelPage) {
            const newFormatRows = document.querySelectorAll('li[class*="tableRow___"]');
            newFormatRows.forEach(row => {
                const userId = extractUserId(row);
                if (!userId || processedUsers.has(userId)) return;
                processedUsers.add(userId);
                fetchQueue.push({ userId, element: row, listType: 'new' });
            });
        }

        if (showTravelLists && onTravelPage) {
            const newFormatRows = document.querySelectorAll('li[class*="tableRow___"]');
            newFormatRows.forEach(row => {
                const userId = extractUserId(row);
                if (!userId || processedUsers.has(userId)) return;
                processedUsers.add(userId);
                fetchQueue.push({ userId, element: row, listType: 'new' });
            });
        }

        if (showTravelLists) {
            const oldFormatLists = document.querySelectorAll('.users-list');
            oldFormatLists.forEach(list => {
                const users = list.querySelectorAll('li');
                users.forEach(userLi => {
                    const userId = extractUserId(userLi);
                    if (!userId || processedUsers.has(userId)) return;
                    processedUsers.add(userId);
                    fetchQueue.push({ userId, element: userLi, listType: 'old' });
                });
            });
        }

        processQueue();
    }

    function setupObserver() {
        const observer = new MutationObserver((mutations) => {
            let shouldScan = false;
            let shouldCheckSettings = false;

            for (const mutation of mutations) {
                if (mutation.addedNodes.length > 0) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.matches?.('ul.settings-menu') ||
                                node.querySelector?.('ul.settings-menu')) {
                                shouldCheckSettings = true;
                            }

                            if (node.matches?.('li[class*="tableRow___"]') ||
                                node.querySelector?.('li[class*="tableRow___"]')) {
                                shouldScan = true;
                            }

                            if (node.classList?.contains('users-list') ||
                                node.querySelector?.('.users-list') ||
                                (node.tagName === 'LI' && node.closest('.users-list'))) {
                                shouldScan = true;
                            }
                        }
                    }
                }
            }

            if (shouldCheckSettings) {
                setTimeout(injectSettingsMenuItem, 50);
            }

            if (shouldScan) {
                setTimeout(scanForUsers, 100);
            }
        });

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

    function init() {
        loadSettings();

        setTimeout(injectSettingsMenuItem, 500);
        setTimeout(scanForUsers, 500);
        setupObserver();
    }

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