Torn Stock Tracker

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();

})();