Torn Stock Tracker

Displays stock profit/loss % on the Stock Market button in the menu.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Torn Stock Tracker
// @namespace    http://tampermonkey.net/
// @version      1.4.1
// @license      MIT
// @description  Displays stock profit/loss % on the Stock Market button in the menu.
// @author       Cypher-[2641265]
// @match        https://www.torn.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        GM_xmlhttpRequest
// @connect      torn.com
// ==/UserScript==

(function() {
    'use strict';

    // --- API Key logic ---
    function getApiKey() {
        return localStorage.getItem('stockTrackerAPIKey') || "";
    }
    function setApiKey(key) {
        localStorage.setItem('stockTrackerAPIKey', key);
    }
    function showApiKeyPopup(onSubmit) {
        // Remove any existing popup
        const oldPopup = document.getElementById('stock-popup');
        if (oldPopup) oldPopup.remove();

        const popup = document.createElement('div');
        popup.id = 'stock-popup';
        popup.style.position = 'fixed';
        popup.style.top = '50%';
        popup.style.left = '50%';
        popup.style.transform = 'translate(-50%, -50%)';
        popup.style.background = '#222';
        popup.style.color = '#fff';
        popup.style.padding = '24px 18px 18px 18px';
        popup.style.borderRadius = '8px';
        popup.style.boxShadow = '0 2px 16px #000a';
        popup.style.zIndex = 99999;
        popup.style.minWidth = '320px';
        popup.style.textAlign = 'center';

        popup.innerHTML = `
            <div style="font-size:1.1em;margin-bottom:10px;">Enter your Torn API Key</div>
            <input id="api-key-input" type="text" placeholder="API Key" style="width:90%;padding:6px;margin-bottom:10px;border-radius:4px;border:1px solid #444;background:#111;color:#fff;">
            <br>
            <button id="api-key-btn" style="padding:6px 18px;border-radius:4px;border:none;background:#4caf50;color:#fff;font-weight:bold;cursor:pointer;">Save</button>
        `;

        document.body.appendChild(popup);

        document.getElementById('api-key-btn').onclick = function() {
            const val = document.getElementById('api-key-input').value.trim();
            if (val) {
                onSubmit(val);
                popup.remove();
            }
        };
        document.getElementById('api-key-input').onkeydown = function(e) {
            if (e.key === 'Enter') {
                document.getElementById('api-key-btn').click();
            }
        };
        document.getElementById('api-key-input').focus();
    }

    // --- Main logic ---
    let apiKey = getApiKey();
    let lastSearch = localStorage.getItem('tornStockSearch') || "";
    let lastPriceData = JSON.parse(localStorage.getItem('tornStockLastPriceData')) || null;

    function startScript() {
        const tornStockAPI = `https://api.torn.com/torn/?selections=stocks&key=${apiKey}`;
        const userStockAPI = `https://api.torn.com/user/?selections=stocks&key=${apiKey}`;

        // Utility: Find the Stock Market button in the Areas menu
        function getStockMarketButton() {
            return document.querySelector('#nav-stock_market .desktopLink___SG2RU .linkName___FoKha');
        }

        // Render the % next to the Stock Market button
        function renderStockPercent(acronym, percent) {
            const btn = getStockMarketButton();
            if (!btn) return;

            // Remove ALL old percent spans in the row to prevent duplicates
            const row = btn.closest('.area-row___iBD8N') || btn.parentElement;
            if (row) {
                row.querySelectorAll('.stock-profit-percent').forEach(el => el.remove());
            }

            let span = document.createElement('span');
            span.className = 'stock-profit-percent';
            span.style.position = "absolute";
            span.style.right = "7px";
            span.style.top = "50%";
            span.style.transform = "translateY(-50%)";
            span.style.pointerEvents = "auto";
            span.style.cursor = "pointer";

            if (typeof percent === "number") {
                const sign = percent > 0 ? "+" : "";
                let color = "#ddd";
                if (percent < 0) {
                    color = "#e53935"; // red for negative
                } else if (percent > 0.5) {
                    color = "#4caf50"; // green for > 0.5%
                }
                span.style.color = color;
                span.textContent = `(${sign}${percent.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}%)`;
            } else {
                // First time use or no stock set
                span.style.color = "#aaa";
                span.textContent = "Setup";
            }

            // Popup on click
            span.onclick = function(e) {
                e.stopPropagation();
                if (!getApiKey()) {
                    showApiKeyPopup(function(key) {
                        setApiKey(key);
                        apiKey = key; // <-- update the main variable
                        // After setting API key, show the stock popup
                        showStockPopup(acronym, function(newSearch) {
                            localStorage.setItem('tornStockSearch', newSearch);
                            lastSearch = newSearch;
                            // update API URLs with new key
                            startScript();
                        });
                    });
                } else {
                    showStockPopup(acronym, function(newSearch) {
                        localStorage.setItem('tornStockSearch', newSearch);
                        lastSearch = newSearch;
                        fetchAndDisplayStockPercent(newSearch);
                    });
                }
            };

            // Ensure the parent is positioned relatively
            if (row.classList && row.classList.contains('area-row___iBD8N')) {
                row.style.position = "relative";
                row.appendChild(span);
            } else {
                btn.parentElement.appendChild(span);
            }
        }

        // Fetch and update the percent
        function fetchAndDisplayStockPercent(searchTerm) {
            if (!searchTerm) {
                renderStockPercent("", null);
                return;
            }
            GM_xmlhttpRequest({
                method: "GET",
                url: tornStockAPI,
                onload: function(stockResponse) {
                    const stockData = JSON.parse(stockResponse.responseText);
                    const stocks = stockData.stocks || {};
                    let foundStock = null;
                    for (const id in stocks) {
                        const stock = stocks[id];
                        if (
                            stock.acronym.toLowerCase() === searchTerm.toLowerCase() ||
                            stock.name.toLowerCase().includes(searchTerm.toLowerCase())
                        ) {
                            foundStock = { ...stock, id };
                            break;
                        }
                    }
                    if (!foundStock) {
                        renderStockPercent("", null);
                        return;
                    }

                    GM_xmlhttpRequest({
                        method: "GET",
                        url: userStockAPI,
                        onload: function(userResponse) {
                            const userData = JSON.parse(userResponse.responseText);
                            const userStocks = userData.stocks || {};
                            const userStock = userStocks[foundStock.id];
                            const currentPrice = Number(foundStock.current_price);
                            let profitLossPercent = 0;
                            if (userStock && userStock.transactions) {
                                let totalShares = 0;
                                let totalCost = 0;
                                for (const txId in userStock.transactions) {
                                    const tx = userStock.transactions[txId];
                                    const shares = Number(tx.shares);
                                    const boughtPrice = Number(tx.bought_price);
                                    if (!isNaN(shares) && !isNaN(boughtPrice)) {
                                        totalShares += shares;
                                        totalCost += shares * boughtPrice;
                                    }
                                }
                                if (totalShares > 0) {
                                    const avgBoughtPrice = totalCost / totalShares;
                                    profitLossPercent = ((currentPrice - avgBoughtPrice) / avgBoughtPrice) * 100;
                                }
                            }
                            renderStockPercent(foundStock.acronym, profitLossPercent);
                            // Save latest data to localStorage
                            localStorage.setItem('tornStockLastPriceData', JSON.stringify({
                                acronym: foundStock.acronym,
                                price: currentPrice,
                                priceColor: profitLossPercent
                            }));
                        },
                        onerror: function() {
                            renderStockPercent(foundStock.acronym, 0);
                            localStorage.setItem('tornStockLastPriceData', JSON.stringify({
                                acronym: foundStock.acronym,
                                price: Number(foundStock.current_price),
                                priceColor: 0
                            }));
                        }
                    });
                },
                onerror: function() {
                    renderStockPercent("", null);
                }
            });
        }

        // Wait for the menu to load, then inject the percent
        function waitForMenuAndUpdate() {
            if (!getApiKey()) {
                renderStockPercent("", null);
                return;
            }
            const btn = getStockMarketButton();
            if (btn) {
                let search = lastSearch;
                if (!search && lastPriceData && lastPriceData.acronym) {
                    search = lastPriceData.acronym;
                }
                fetchAndDisplayStockPercent(search);
            } else {
                setTimeout(waitForMenuAndUpdate, 500);
            }
        }

        // Update on page load and every 5 minutes
        waitForMenuAndUpdate();
        setInterval(waitForMenuAndUpdate, 300000);

        // Optional: re-inject on SPA navigation (if Torn uses AJAX navigation)
        document.body.addEventListener('click', function(e) {
            setTimeout(waitForMenuAndUpdate, 1000);
        });

        // --- Popup logic ---
        function showStockPopup(currentValue, onSubmit) {
            // Remove any existing popup
            const oldPopup = document.getElementById('stock-popup');
            if (oldPopup) oldPopup.remove();

            const popup = document.createElement('div');
            popup.id = 'stock-popup';
            popup.style.position = 'fixed';
            popup.style.top = '50%';
            popup.style.left = '50%';
            popup.style.transform = 'translate(-50%, -50%)';
            popup.style.background = '#222';
            popup.style.color = '#fff';
            popup.style.padding = '24px 18px 18px 18px';
            popup.style.borderRadius = '8px';
            popup.style.boxShadow = '0 2px 16px #000a';
            popup.style.zIndex = 99999;
            popup.style.minWidth = '260px';
            popup.style.textAlign = 'center';

            popup.innerHTML = `
                <div style="font-size:1.1em;margin-bottom:10px;">Track a different stock</div>
                <input id="stock-popup-input" type="text" value="${currentValue || ''}" placeholder="Stock acronym or name" style="width:90%;padding:6px;margin-bottom:10px;border-radius:4px;border:1px solid #444;background:#111;color:#fff;">
                <br>
                <button id="stock-popup-btn" style="padding:6px 18px;border-radius:4px;border:none;background:#4caf50;color:#fff;font-weight:bold;cursor:pointer;">Track</button>
                <button id="stock-popup-cancel" style="padding:6px 12px;border-radius:4px;border:none;background:#888;color:#fff;margin-left:8px;cursor:pointer;">Cancel</button>
            `;

            document.body.appendChild(popup);

            document.getElementById('stock-popup-btn').onclick = function() {
                const val = document.getElementById('stock-popup-input').value.trim();
                if (val) {
                    onSubmit(val);
                    popup.remove();
                }
            };
            document.getElementById('stock-popup-cancel').onclick = function() {
                popup.remove();
            };
            document.getElementById('stock-popup-input').onkeydown = function(e) {
                if (e.key === 'Enter') {
                    document.getElementById('stock-popup-btn').click();
                }
            };
            document.getElementById('stock-popup-input').focus();
        }
    }

    // --- Entry point: do NOT prompt for API key on load, just start script ---
    startScript();

})();