Torn Stock Tracker

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Torn Stock Tracker
// @namespace    http://tampermonkey.net/
// @version      1.5.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 Limited 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() {
            const stockNav = document.querySelector('#nav-stock_market');
            if (!stockNav) return null;

            return (
                stockNav.querySelector('[class*="desktopLink"] [class*="linkName"]') ||
                stockNav.querySelector('[class*="desktopLink"] a') ||
                stockNav.querySelector('a')
            );
        }

        // Utility: Find the Stock Market button in mobile view
        function getMobileStockMarketButton() {
            const stockNav = document.querySelector('#nav-stock_market');
            if (!stockNav) return null;

            return (
                stockNav.querySelector('[class*="mobileLink"]') ||
                stockNav.querySelector('[class*="area-mobile"] a') ||
                stockNav.querySelector('[class*="area-mobile"]')
            );
        }

        function getStockRowContainer() {
            const stockNav = document.querySelector('#nav-stock_market');
            if (!stockNav) return null;

            return (
                stockNav.querySelector('[class*="area-desktop"] [class*="area-row"]') ||
                stockNav.querySelector('[class*="area-row"]') ||
                stockNav
            );
        }

        function getMobileRowContainer() {
            const stockNav = document.querySelector('#nav-stock_market');
            if (!stockNav) return null;

            return (
                stockNav.querySelector('[class*="area-mobile"] [class*="area-row"]') ||
                stockNav.querySelector('[class*="area-mobile"]') ||
                stockNav
            );
        }

        // Render the % next to the Stock Market button (desktop only)
        function renderStockPercent(acronym, percent) {
            // Only render if we're NOT in mobile view
            if (isMobileView()) return;

            const btn = getStockMarketButton();
            if (!btn) return;

            // Remove ALL old percent spans in the row to prevent duplicates
            const row = getStockRowContainer() || 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) {
                row.style.position = "relative";
                row.appendChild(span);
            } else {
                btn.parentElement.appendChild(span);
            }
        }

        // Check if we're in mobile view
        function isMobileView() {
            // Check if we have the mobile areas wrapper
            const mobileWrapper = document.querySelector('#nav-stock_market [class*="area-mobile"]');
            if (!mobileWrapper) return false;

            // Check if the mobile wrapper is actually visible (not display: none)
            const mobileStyle = window.getComputedStyle(mobileWrapper);
            if (mobileStyle.display === 'none') return false;

            // Also check if desktop area is hidden (more reliable check)
            const desktopArea = document.querySelector('#nav-stock_market [class*="area-desktop"]');
            if (desktopArea) {
                const desktopStyle = window.getComputedStyle(desktopArea);
                if (desktopStyle.display !== 'none') return false;
            }

            return true;
        }

        // Render mobile indicator
        function renderMobileIndicator(percent) {
            // Only render if we're actually in mobile view
            if (!isMobileView()) return;

            // Find the stock market mobile container using the exact structure from your HTML
            const stockMarketContainer = document.querySelector('#nav-stock_market');
            if (!stockMarketContainer) return;

            const mobileRowContainer = getMobileRowContainer();
            if (!mobileRowContainer) return;

            // Remove existing mobile indicator
            const existing = mobileRowContainer.querySelector('.mobileStockProfit');
            if (existing) existing.remove();

            if (typeof percent !== "number") return;

            const indicator = document.createElement('div');
            indicator.className = 'mobileStockProfit';
            indicator.style.cssText = `
                position: absolute;
                top: 2px;
                right: 2px;
                z-index: 10;
                pointer-events: none;
            `;

            let symbol = "";
            let color = "#ddd";

            if (percent < 0) {
                symbol = "✕"; // X for negative
                color = "#eb6c6aff";
            } else if (percent === 0 || Math.abs(percent) < 0.01) {
                symbol = "−"; // Dash for neutral
                color = "#ddd";
            } else if (percent > 0.5) {
                symbol = "✓"; // Checkmark for positive > 0.5%
                color = "#7edb81ff";
            } else {
                symbol = "−"; // Dash for small positive
                color = "#ddd";
            }

            indicator.innerHTML = `<span style="color: ${color}; font-size: 10px; font-weight: bold;">${symbol}</span>`;

            // Make sure the parent container is positioned relatively
            mobileRowContainer.style.position = 'relative';
            mobileRowContainer.appendChild(indicator);
        }

        // Fetch and update the percent
        function fetchAndDisplayStockPercent(searchTerm) {
            if (!searchTerm) {
                renderStockPercent("", null);
                renderMobileIndicator(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);
                        renderMobileIndicator(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) {
                                const transactions = userStock.transactions;
                                const transactionCount = Object.keys(transactions).length;

                                if (transactionCount > 1) {
                                    // Multiple transactions: find the largest one by shares
                                    let largestTransaction = null;
                                    let maxShares = 0;

                                    for (const txId in transactions) {
                                        const tx = transactions[txId];
                                        const shares = Number(tx.shares);
                                        if (!isNaN(shares) && shares > maxShares) {
                                            maxShares = shares;
                                            largestTransaction = tx;
                                        }
                                    }

                                    if (largestTransaction) {
                                        const boughtPrice = Number(largestTransaction.bought_price);
                                        if (!isNaN(boughtPrice)) {
                                            profitLossPercent = ((currentPrice - boughtPrice) / boughtPrice) * 100;
                                        }
                                    }
                                } else {
                                    // Single transaction: use the normal calculation
                                    for (const txId in transactions) {
                                        const tx = transactions[txId];
                                        const boughtPrice = Number(tx.bought_price);
                                        if (!isNaN(boughtPrice)) {
                                            profitLossPercent = ((currentPrice - boughtPrice) / boughtPrice) * 100;
                                            break;
                                        }
                                    }
                                }
                            }
                            renderStockPercent(foundStock.acronym, profitLossPercent);
                            renderMobileIndicator(profitLossPercent);
                            // Save latest data to localStorage
                            localStorage.setItem('tornStockLastPriceData', JSON.stringify({
                                acronym: foundStock.acronym,
                                price: currentPrice,
                                priceColor: profitLossPercent
                            }));
                        },
                        onerror: function() {
                            renderStockPercent(foundStock.acronym, 0);
                            renderMobileIndicator(0);
                            localStorage.setItem('tornStockLastPriceData', JSON.stringify({
                                acronym: foundStock.acronym,
                                price: Number(foundStock.current_price),
                                priceColor: 0
                            }));
                        }
                    });
                },
                onerror: function() {
                    renderStockPercent("", null);
                    renderMobileIndicator(null);
                }
            });
        }

        // Wait for the menu to load, then inject the percent
        function waitForMenuAndUpdate() {
            if (!getApiKey()) {
                renderStockPercent("", null);
                renderMobileIndicator(null);
                return;
            }
            const btn = getStockMarketButton();
            const mobileBtn = getMobileStockMarketButton();

            if (btn || mobileBtn) {
                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();

})();