Torn Stock Tracker

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

当前为 2025-06-26 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})();