您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays stock profit/loss % on the Stock Market button in the menu.
// ==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(); })();