// ==UserScript==
// @name Torn Status Monitor
// @namespace TornStatusMonitor
// @version 1.9
// @description Displays Energy (Green), Nerve (Red), and Happiness (Yellow) with time-to-full timers. GUI is minimizable/draggable. Updates every 2 minutes.
// @author GNSC4 [268863]
// @match https://www.torn.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @connect api.torn.com
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- Configuration ---
const UPDATE_INTERVAL_MS = 2 * 60 * 1000; // How often to fetch API data (2 minutes)
const API_KEY_STORAGE = 'torn_status_api_key_v1'; // Storage key for API key
const GUI_MINIMIZED_STORAGE = 'torn_status_gui_minimized_v1'; // Storage key for minimized state
const GUI_POSITION_STORAGE = 'torn_status_gui_position_v1'; // Storage key for GUI position
const DEFAULT_POSITION = { top: '10px', left: '10px' }; // Default position
// --- State Variables ---
let apiKey = GM_getValue(API_KEY_STORAGE, null); // Load saved API key
let isMinimized = GM_getValue(GUI_MINIMIZED_STORAGE, false); // Load saved minimized state
let guiPosition = JSON.parse(GM_getValue(GUI_POSITION_STORAGE, JSON.stringify(DEFAULT_POSITION))); // Load saved position or use default
// --- Position Validation ---
// Check if the loaded position is valid; reset if not.
try {
const topPx = parseInt(guiPosition.top, 10);
const leftPx = parseInt(guiPosition.left, 10);
// Basic check: ensure position is not negative or excessively large (adjust max values if needed)
if (isNaN(topPx) || isNaN(leftPx) || topPx < 0 || leftPx < 0 || topPx > (window.innerHeight || 2000) || leftPx > (window.innerWidth || 3000)) {
console.warn("Torn Status Monitor: Invalid saved position detected. Resetting to default.", guiPosition);
guiPosition = { ...DEFAULT_POSITION }; // Use spread to copy default
GM_setValue(GUI_POSITION_STORAGE, JSON.stringify(guiPosition)); // Save the reset position
}
} catch (e) {
console.error("Torn Status Monitor: Error validating position, resetting.", e);
guiPosition = { ...DEFAULT_POSITION };
GM_setValue(GUI_POSITION_STORAGE, JSON.stringify(guiPosition));
}
let intervals = { update: null, energy: null, nerve: null, happiness: null }; // Holds timer intervals
// --- GUI Element References ---
let guiContainer, minimizeButton, apiKeyInput, apiKeySaveButton;
let energyDisplay, nerveDisplay, happinessDisplay;
let energyTimerDisplay, nerveTimerDisplay, happinessTimerDisplay;
// --- CSS Styles ---
function addStyles() {
// Inject CSS styles for the GUI element. Uses !important extensively to override Torn styles.
// Use the validated guiPosition here
const css = `
#torn-status-gui {
position: fixed !important;
top: ${guiPosition.top} !important;
left: ${guiPosition.left} !important;
background-color: rgba(30, 30, 30, 0.9) !important; /* Darker background */
border: 1px solid #999 !important; /* Lighter border */
border-radius: 5px !important;
padding: 10px !important;
color: #ddd !important; /* Lighter text */
font-family: Arial, sans-serif !important;
font-size: 12px !important;
z-index: 99999 !important; /* High z-index */
min-width: 190px !important; /* Slightly wider */
box-shadow: 0 2px 15px rgba(0,0,0,0.6) !important; /* Enhanced shadow */
cursor: grab !important; /* Default cursor indicates draggable */
user-select: none !important; /* Prevent text selection */
box-sizing: border-box !important; /* Consistent box model */
}
/* Minimized state styles */
#torn-status-gui.minimized {
padding: 0 !important;
min-width: 0 !important;
width: 35px !important; /* Slightly larger minimized size */
height: 35px !important;
overflow: hidden !important;
cursor: pointer !important; /* Indicate it can be clicked to maximize */
}
#torn-status-gui.minimized #torn-status-content,
#torn-status-gui.minimized #torn-status-api-setup,
#torn-status-gui.minimized #torn-status-header h3 { /* Hide title when minimized */
display: none !important;
}
#torn-status-gui.minimized #torn-status-header {
padding: 0 !important; /* Remove padding in minimized header */
margin: 0 !important;
border-bottom: none !important; /* Remove border when minimized */
min-height: 35px !important; /* Ensure header takes full height */
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
#torn-status-gui.minimized #torn-status-minimize-btn {
position: static !important; /* Reset position */
margin: 0 !important;
padding: 5px !important; /* Make button easier to click */
font-size: 16px !important; /* Larger icon */
line-height: 1 !important;
}
/* Header styles */
#torn-status-header {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
border-bottom: 1px solid #666 !important;
margin-bottom: 8px !important; /* Increased spacing */
padding-bottom: 8px !important;
cursor: grab !important; /* Cursor for dragging */
}
#torn-status-header h3 {
margin: 0 !important;
font-size: 14px !important;
font-weight: bold !important;
color: #fff !important; /* White title */
}
/* Minimize button styles */
#torn-status-minimize-btn {
background: #555 !important; /* Darker button */
border: 1px solid #777 !important;
color: #fff !important;
cursor: pointer !important;
padding: 3px 6px !important; /* Slightly larger padding */
border-radius: 3px !important;
font-weight: bold !important;
line-height: 1 !important;
}
#torn-status-minimize-btn:hover {
background: #777 !important; /* Lighter hover */
}
/* Content area styles */
#torn-status-content div, #torn-status-api-setup div {
margin-bottom: 5px !important; /* Consistent spacing */
line-height: 1.4 !important; /* Improve readability */
}
#torn-status-content span:first-child { /* Label styling */
display: inline-block !important;
min-width: 55px !important; /* Adjusted width for labels */
font-weight: bold !important;
color: #aaa !important; /* Grey labels */
}
#torn-status-content .value { /* Base style for Current/Max value */
display: inline-block !important;
min-width: 65px !important; /* Width for values */
text-align: right !important;
margin-right: 5px !important;
font-weight: bold !important; /* Make values bold */
}
/* --- Color Coding for Values --- */
#torn-status-content .energy-value {
color: #99dd99 !important; /* Green for Energy */
}
#torn-status-content .nerve-value {
color: #ff6666 !important; /* Red for Nerve */
}
#torn-status-content .happy-value {
color: #ffff99 !important; /* Yellow for Happiness */
}
/* --- --- */
#torn-status-content .timer { /* Timer styling */
font-weight: bold !important;
color: #99dd99 !important; /* Lighter green for timers */
margin-left: 5px !important;
}
#torn-status-content .timer.full {
color: #ffff99 !important; /* Yellow when full */
}
#torn-status-content .error, #torn-status-api-setup .error {
color: #ff6666 !important; /* Red for errors */
font-weight: bold !important;
margin-top: 5px !important;
}
/* API Key input section styles */
#torn-status-api-setup {
padding-top: 5px !important;
}
#torn-status-api-setup input[type="text"] {
padding: 4px 6px !important; /* More padding */
border: 1px solid #888 !important;
border-radius: 3px !important;
margin-right: 5px !important;
width: calc(100% - 65px) !important; /* Calculate width based on button */
background-color: #fff !important;
color: #000 !important;
box-sizing: border-box !important;
}
#torn-status-api-setup button {
padding: 4px 10px !important; /* More padding */
border: 1px solid #999 !important;
border-radius: 3px !important;
background-color: #ccc !important; /* Lighter button */
color: #000 !important;
cursor: pointer !important;
font-weight: bold !important;
box-sizing: border-box !important;
}
#torn-status-api-setup button:hover {
background-color: #ddd !important;
}
#torn-status-api-setup p {
margin-bottom: 8px !important;
font-size: 11px !important;
color: #ccc !important;
}
#torn-status-api-setup a {
color: #aaa !important;
text-decoration: underline !important;
}
#torn-status-api-setup a:hover {
color: #ccc !important;
}
`;
GM_addStyle(css); // Add the styles to the page
}
// --- GUI Creation ---
function createGUI() {
console.log("Torn Status Monitor: Attempting to create GUI elements...");
// Create the main container div
guiContainer = document.createElement('div');
guiContainer.id = 'torn-status-gui';
if (isMinimized) {
guiContainer.classList.add('minimized'); // Apply minimized class if needed
}
// Create Header
const header = document.createElement('div');
header.id = 'torn-status-header';
const title = document.createElement('h3');
title.textContent = 'Status'; // Shorter title
minimizeButton = document.createElement('button');
minimizeButton.id = 'torn-status-minimize-btn';
minimizeButton.textContent = isMinimized ? '□' : '−'; // Use symbols for minimize/maximize
minimizeButton.title = isMinimized ? 'Maximize' : 'Minimize';
minimizeButton.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent drag from starting on button click
toggleMinimize();
});
header.appendChild(title);
header.appendChild(minimizeButton);
guiContainer.appendChild(header);
// Add click listener to header for maximizing when minimized
header.addEventListener('click', () => {
if (isMinimized) {
toggleMinimize();
}
});
// Create Content Area (switches between API setup and status)
const contentArea = document.createElement('div');
contentArea.id = 'torn-status-content-area';
// --- API Setup Section (Initially hidden if key exists) ---
const apiSetupDiv = document.createElement('div');
apiSetupDiv.id = 'torn-status-api-setup';
apiSetupDiv.style.display = apiKey ? 'none' : 'block !important';
apiSetupDiv.innerHTML = `<p>Enter Torn API Key (<a href="https://www.torn.com/preferences.php#tab=api" target="_blank" title="Go to API Key settings">Get Key</a>):</p>`;
const inputGroup = document.createElement('div'); // Group input and button
apiKeyInput = document.createElement('input');
apiKeyInput.type = 'text';
apiKeyInput.placeholder = 'Your API Key';
apiKeyInput.addEventListener('keypress', (e) => { // Allow saving with Enter key
if (e.key === 'Enter') {
saveApiKey();
}
});
apiKeySaveButton = document.createElement('button');
apiKeySaveButton.textContent = 'Save';
apiKeySaveButton.title = 'Save API Key';
apiKeySaveButton.addEventListener('click', saveApiKey);
inputGroup.appendChild(apiKeyInput);
inputGroup.appendChild(apiKeySaveButton);
apiSetupDiv.appendChild(inputGroup);
const apiErrorDiv = document.createElement('div'); // For displaying API key errors
apiErrorDiv.className = 'error api-error'; // Add class for specific targeting
apiSetupDiv.appendChild(apiErrorDiv);
contentArea.appendChild(apiSetupDiv);
// --- Status Display Section (Initially hidden if no key) ---
const statusDiv = document.createElement('div');
statusDiv.id = 'torn-status-content';
statusDiv.style.display = apiKey ? 'block !important' : 'none !important';
// Create display elements for each bar, adding specific classes for color coding
energyDisplay = document.createElement('div');
nerveDisplay = document.createElement('div');
happinessDisplay = document.createElement('div');
// Add specific classes like 'energy-value', 'nerve-value', 'happy-value'
energyDisplay.innerHTML = '<span>Energy:</span><span class="value energy-value">--/--</span>';
nerveDisplay.innerHTML = '<span>Nerve:</span><span class="value nerve-value">--/--</span>';
happinessDisplay.innerHTML = '<span>Happy:</span><span class="value happy-value">--/--</span>';
// Create timer elements
energyTimerDisplay = document.createElement('span');
energyTimerDisplay.className = 'timer';
energyTimerDisplay.textContent = '--:--:--';
energyDisplay.appendChild(energyTimerDisplay);
nerveTimerDisplay = document.createElement('span');
nerveTimerDisplay.className = 'timer';
nerveTimerDisplay.textContent = '--:--:--';
nerveDisplay.appendChild(nerveTimerDisplay);
happinessTimerDisplay = document.createElement('span');
happinessTimerDisplay.className = 'timer';
happinessTimerDisplay.textContent = '--:--:--';
happinessDisplay.appendChild(happinessTimerDisplay);
// Add elements to the status section
statusDiv.appendChild(energyDisplay);
statusDiv.appendChild(nerveDisplay);
statusDiv.appendChild(happinessDisplay);
const statusErrorDiv = document.createElement('div'); // For displaying general fetch errors
statusErrorDiv.className = 'error status-error';
statusDiv.appendChild(statusErrorDiv);
contentArea.appendChild(statusDiv);
// Add content area to the main container
guiContainer.appendChild(contentArea);
// Add the GUI to the page body
try {
if (!document.body) {
throw new Error("Document body not found yet.");
}
document.body.appendChild(guiContainer);
console.log("Torn Status Monitor: GUI appended to body.");
} catch (error) {
console.error("Torn Status Monitor: Failed to append GUI to document body.", error);
return; // Stop if we can't even append the GUI
}
// Make the GUI draggable using the header as the handle
makeDraggable(guiContainer, header);
console.log("Torn Status Monitor: GUI creation complete.");
}
// --- Drag and Drop Functionality ---
function makeDraggable(element, handle) {
let startX = 0, startY = 0, initialTop = 0, initialLeft = 0;
// Use addEventListener for mousedown on the handle
handle.addEventListener('mousedown', dragMouseDown);
function dragMouseDown(e) {
console.log("Torn Status Monitor: dragMouseDown triggered!");
// Ignore clicks on the minimize button itself
if (e.target === minimizeButton) return;
e = e || window.event;
// e.preventDefault(); // Keep this commented out for now
// Get the initial mouse cursor position
startX = e.clientX;
startY = e.clientY;
// Get the initial element position (parse from style or use validated guiPosition)
initialTop = parseInt(element.style.top || guiPosition.top, 10);
initialLeft = parseInt(element.style.left || guiPosition.left, 10);
// Fallback if parsing failed
if (isNaN(initialTop)) initialTop = parseInt(DEFAULT_POSITION.top, 10);
if (isNaN(initialLeft)) initialLeft = parseInt(DEFAULT_POSITION.left, 10);
// Set up event listeners for mouse movement and release on the document
document.addEventListener('mouseup', closeDragElement);
document.addEventListener('mousemove', elementDrag);
// Change cursor style to indicate dragging
element.style.cursor = 'grabbing !important';
handle.style.cursor = 'grabbing !important';
console.log(`Torn Status Monitor: Drag start - Initial Pos: (${initialLeft}px, ${initialTop}px), Mouse: (${startX}, ${startY})`);
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault(); // Prevent text selection during drag
// Calculate the distance the mouse has moved
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
// Calculate the element's new position based on initial position + delta
let newTop = initialTop + deltaY;
let newLeft = initialLeft + deltaX;
// --- Boundary checks ---
newTop = Math.max(0, newTop);
newLeft = Math.max(0, newLeft);
newTop = Math.min(newTop, window.innerHeight - 20);
newLeft = Math.min(newLeft, window.innerWidth - 20);
// --- End Boundary checks ---
// Set the element's new position (Re-added !important)
// Use setProperty for better compatibility with !important
element.style.setProperty('top', newTop + "px", 'important');
element.style.setProperty('left', newLeft + "px", 'important');
}
function closeDragElement() {
console.log("Torn Status Monitor: closeDragElement triggered!");
// Remove event listeners from the document
document.removeEventListener('mouseup', closeDragElement);
document.removeEventListener('mousemove', elementDrag);
// Restore default cursor styles
element.style.cursor = 'grab !important';
handle.style.cursor = 'grab !important';
// Get the final position directly from the style
const finalTop = element.style.top;
const finalLeft = element.style.left;
// Basic check to ensure we save valid pixel values
if (finalTop && finalLeft && finalTop.endsWith('px') && finalLeft.endsWith('px')) {
guiPosition = { top: finalTop, left: finalLeft };
GM_setValue(GUI_POSITION_STORAGE, JSON.stringify(guiPosition));
console.log("Torn Status Monitor: Drag ended. Position saved:", guiPosition);
} else {
console.error("Torn Status Monitor: Drag ended but final position style was invalid. Not saving.", { top: finalTop, left: finalLeft });
// Attempt to re-apply last known valid position if save failed
element.style.setProperty('top', guiPosition.top, 'important'); // Revert to last saved or default
element.style.setProperty('left', guiPosition.left, 'important');
}
}
}
// --- API Interaction ---
function fetchData() {
// Abort if API key is not set
if (!apiKey) {
console.warn("Torn Status Monitor: API Key not set. Aborting fetch.");
updateDisplay({ error: "API Key needed", target: 'api' }); // Show error in API section
switchView(false); // Ensure API input view is shown
return;
}
// Construct the API URL
const url = `https://api.torn.com/user/?selections=bars&key=${apiKey}&comment=TornStatusMonitorScript`;
// console.log("Torn Status Monitor: Fetching data..."); // Less verbose logging
// Use GM_xmlhttpRequest for cross-origin requests
GM_xmlhttpRequest({
method: "GET",
url: url,
timeout: 15000, // 15 second timeout
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
console.log("Torn Status Monitor: API Response:", data); // Log the received data
// Check for API-level errors in the response
if (data.error) {
console.error("Torn Status Monitor API Error:", data.error.code, data.error.error);
const errorMessage = `API Error ${data.error.code}`;
updateDisplay({ error: errorMessage, target: 'api' }); // Show error in API section
// If the key is incorrect (code 2), clear it and switch view
if (data.error.code === 2) {
apiKey = null;
GM_setValue(API_KEY_STORAGE, null); // Clear invalid key
switchView(false); // Show API input
}
clearAllTimers(); // Stop timers on error
} else {
// Process successful data
updateDisplay(data); // Update GUI values
startTimers(data); // Start/reset countdown timers
// Clear any previous error messages
clearErrorMessages();
// Ensure the status view is visible if data is successfully fetched
if (!isMinimized) {
switchView(true);
}
}
} catch (e) {
console.error("Torn Status Monitor: Error parsing API response:", e);
updateDisplay({ error: "Parse Error", target: 'status' }); // Show error in status section
clearAllTimers();
}
},
onerror: function(response) {
console.error("Torn Status Monitor: GM_xmlhttpRequest error:", response);
updateDisplay({ error: "Network Error", target: 'status' }); // Show error in status section
clearAllTimers();
},
ontimeout: function() {
console.error("Torn Status Monitor: API request timed out.");
updateDisplay({ error: "Timeout", target: 'status' }); // Show error in status section
clearAllTimers();
}
});
}
// --- Display Update Logic ---
function updateDisplay(data) {
// Ensure GUI elements exist before trying to update them
if (!guiContainer) return;
// Use the specific classes for value spans
const energyValSpan = guiContainer.querySelector('.energy-value');
const nerveValSpan = guiContainer.querySelector('.nerve-value');
const happyValSpan = guiContainer.querySelector('.happy-value');
const apiErrorDiv = guiContainer.querySelector('.api-error');
const statusErrorDiv = guiContainer.querySelector('.status-error');
// Clear previous errors first
if (apiErrorDiv) apiErrorDiv.textContent = '';
if (statusErrorDiv) statusErrorDiv.textContent = '';
// Handle and display errors
if (data.error) {
const errorTargetDiv = data.target === 'api' ? apiErrorDiv : statusErrorDiv;
if (errorTargetDiv) {
errorTargetDiv.textContent = `Error: ${data.error}`;
}
// Reset value displays on error
if (energyValSpan) energyValSpan.textContent = '--/--';
if (nerveValSpan) nerveValSpan.textContent = '--/--';
if (happyValSpan) happyValSpan.textContent = '--/--';
// Reset timer displays on error - handled by clearAllTimers called by fetchData
return; // Stop further processing
}
// Update bar values if data is valid and elements exist
if (data.energy && typeof data.energy.current !== 'undefined' && typeof data.energy.maximum !== 'undefined' && energyValSpan) {
energyValSpan.textContent = `${data.energy.current}/${data.energy.maximum}`;
} else if (energyValSpan) {
energyValSpan.textContent = 'N/A'; // Indicate if data is missing
}
if (data.nerve && typeof data.nerve.current !== 'undefined' && typeof data.nerve.maximum !== 'undefined' && nerveValSpan) {
nerveValSpan.textContent = `${data.nerve.current}/${data.nerve.maximum}`;
} else if (nerveValSpan) {
nerveValSpan.textContent = 'N/A';
}
if (data.happy && typeof data.happy.current !== 'undefined' && typeof data.happy.maximum !== 'undefined' && happyValSpan) {
happyValSpan.textContent = `${data.happy.current}/${data.happy.maximum}`;
} else if (happyValSpan) {
happyValSpan.textContent = 'N/A';
}
// Timer updates are handled separately by startTimers/updateTimer
}
// --- Timer Logic ---
function startTimers(data) {
// Clear any existing timer intervals before starting new ones
clearAllTimers();
// Always call updateTimer if the bar data and display element exist.
// updateTimer itself will handle displaying "Full" or the countdown.
if (data.energy && typeof data.energy.fulltime !== 'undefined' && energyTimerDisplay) {
// Pass the remaining seconds directly to updateTimer
updateTimer('energy', data.energy.fulltime, energyTimerDisplay);
} else if (energyTimerDisplay) {
// Handle case where energy data might be missing entirely from API response
energyTimerDisplay.textContent = "N/A";
energyTimerDisplay.classList.remove('full');
}
if (data.nerve && typeof data.nerve.fulltime !== 'undefined' && nerveTimerDisplay) {
updateTimer('nerve', data.nerve.fulltime, nerveTimerDisplay);
} else if (nerveTimerDisplay) {
nerveTimerDisplay.textContent = "N/A";
nerveTimerDisplay.classList.remove('full');
}
if (data.happy && typeof data.happy.fulltime !== 'undefined' && happinessTimerDisplay) {
updateTimer('happiness', data.happy.fulltime, happinessTimerDisplay);
} else if (happinessTimerDisplay) {
happinessTimerDisplay.textContent = "N/A";
happinessTimerDisplay.classList.remove('full');
}
}
// Recursive function to update a single timer display every second
// The 'initialSecondsRemaining' parameter now represents the seconds left *at the time of the API call*
function updateTimer(barName, initialSecondsRemaining, displayElement) {
if (!displayElement) return; // Exit if the display element is missing
// Convert the initial value to a number
let secondsRemaining = Number(initialSecondsRemaining);
// Check if secondsRemaining is a valid number
if (isNaN(secondsRemaining)) {
console.error(`Torn Status Monitor: Invalid initialSecondsRemaining for ${barName}`, { initialSecondsRemaining });
displayElement.textContent = "Error";
displayElement.classList.remove('full');
intervals[barName] = null;
return;
}
// --- Timer Tick Function ---
function tick() {
if (secondsRemaining <= 0) {
// Bar is full or past full time
displayElement.textContent = "Full";
displayElement.classList.add('full');
intervals[barName] = null; // Clear the interval reference
} else {
// Bar is still regenerating
displayElement.classList.remove('full');
// Calculate hours, minutes, seconds
const hours = Math.floor(secondsRemaining / 3600);
const minutes = Math.floor((secondsRemaining % 3600) / 60);
const seconds = secondsRemaining % 60;
// Format as HH:MM:SS
displayElement.textContent =
`${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
// Decrement the remaining seconds
secondsRemaining--;
// Schedule the next tick 1 second later using setTimeout
intervals[barName] = setTimeout(tick, 1000);
}
}
// --- End Timer Tick Function ---
// Start the first tick
tick();
}
// Function to clear all active timer intervals
function clearAllTimers() {
clearTimeout(intervals.energy);
clearTimeout(intervals.nerve);
clearTimeout(intervals.happiness);
intervals.energy = null;
intervals.nerve = null;
intervals.happiness = null;
// Also reset timer displays visually if needed
if(energyTimerDisplay) {
energyTimerDisplay.textContent = '--:--:--';
energyTimerDisplay.classList.remove('full');
}
if(nerveTimerDisplay) {
nerveTimerDisplay.textContent = '--:--:--';
nerveTimerDisplay.classList.remove('full');
}
if(happinessTimerDisplay) {
happinessTimerDisplay.textContent = '--:--:--';
happinessTimerDisplay.classList.remove('full');
}
}
// Function to clear error messages from the GUI
function clearErrorMessages() {
if (!guiContainer) return;
const apiErrorDiv = guiContainer.querySelector('.api-error');
const statusErrorDiv = guiContainer.querySelector('.status-error');
if (apiErrorDiv) apiErrorDiv.textContent = '';
if (statusErrorDiv) statusErrorDiv.textContent = '';
}
// --- GUI Interaction Functions ---
function toggleMinimize() {
isMinimized = !isMinimized; // Toggle the state
// Add or remove the 'minimized' class based on the state
guiContainer.classList.toggle('minimized', isMinimized);
// Change the button text/icon
minimizeButton.textContent = isMinimized ? '□' : '−';
minimizeButton.title = isMinimized ? 'Maximize' : 'Minimize';
// Save the new state
GM_setValue(GUI_MINIMIZED_STORAGE, isMinimized);
// Explicitly manage display of content vs API setup when maximizing
if (!isMinimized) {
switchView(!!apiKey); // Show status if key exists, else show API setup
}
}
function saveApiKey() {
const newKey = apiKeyInput.value.trim(); // Get and trim the entered key
if (newKey && /^[a-zA-Z0-9]{16}$/.test(newKey)) { // Basic validation (16 alphanumeric chars)
apiKey = newKey;
GM_setValue(API_KEY_STORAGE, apiKey); // Save the valid key
apiKeyInput.value = ''; // Clear the input field
console.log("Torn Status Monitor: API Key saved.");
clearErrorMessages(); // Clear any previous key errors
switchView(true); // Switch to the status display view
fetchData(); // Fetch data immediately with the new key
// Ensure the periodic update interval is running (or start it)
if (intervals.update) {
clearInterval(intervals.update); // Clear existing interval if any
}
intervals.update = setInterval(fetchData, UPDATE_INTERVAL_MS); // Start new interval
} else {
// Show an error message if the key is invalid
updateDisplay({ error: "Invalid Key format (should be 16 letters/numbers)", target: 'api' });
console.error("Torn Status Monitor: Invalid API Key format entered.");
}
}
// Helper function to switch between API input view and status display view
function switchView(showStatus) {
if (!guiContainer) return; // Make sure GUI exists
const statusDiv = document.getElementById('torn-status-content');
const apiSetupDiv = document.getElementById('torn-status-api-setup');
if (statusDiv && apiSetupDiv) {
// Use !important to ensure styles apply
statusDiv.style.display = showStatus ? 'block !important' : 'none !important';
apiSetupDiv.style.display = showStatus ? 'none !important' : 'block !important';
}
}
// --- Initialization ---
function init() {
try { // Add try...catch around the main init logic
console.log("Torn Status Monitor: Initializing script...");
addStyles(); // Add CSS to the page (uses validated guiPosition)
createGUI(); // Create the HTML structure for the GUI
// If GUI creation failed (e.g., couldn't append), stop here
if (!guiContainer || !document.getElementById('torn-status-gui')) {
console.error("Torn Status Monitor: GUI container not found after creation attempt. Aborting init.");
return;
}
// Apply the potentially reset position from CSS to the element directly
// This ensures the element's style matches the CSS rule if position was reset
// Use validated guiPosition which might have been reset
guiContainer.style.top = guiPosition.top; // No !important needed here, CSS has it
guiContainer.style.left = guiPosition.left;
// Perform initial data fetch if API key exists
if (apiKey) {
// Clear any previously running update interval before starting a new one
if (intervals.update) {
clearInterval(intervals.update);
}
fetchData(); // Fetch data on load
// Set interval for periodic updates with the new frequency
intervals.update = setInterval(fetchData, UPDATE_INTERVAL_MS);
} else {
// If no key, the GUI will show the API input section by default
console.log("Torn Status Monitor: No API Key found. Waiting for user input.");
switchView(false); // Explicitly show API input view
}
console.log("Torn Status Monitor: Initialization complete.");
} catch (error) {
console.error("Torn Status Monitor: CRITICAL ERROR during initialization:", error);
}
}
// --- Run Initialization ---
function runInit() {
try { // Add try...catch around the init trigger itself
// Check if the script has already run (e.g., to prevent multiple instances in some edge cases)
if (document.getElementById('torn-status-gui')) {
console.log("Torn Status Monitor: GUI already exists. Skipping initialization.");
return;
}
console.log("Torn Status Monitor: DOM ready or loaded. Running init().");
init();
} catch (error) {
console.error("Torn Status Monitor: CRITICAL ERROR setting up initialization:", error);
}
}
// Ensure the script runs after the page DOM is ready
if (document.readyState === 'complete' || document.readyState === 'interactive') {
// If already loaded, initialize immediately (wrapped in error handler)
// Use a small timeout to potentially avoid race conditions with Torn's own scripts
setTimeout(runInit, 150); // Slightly increased timeout just in case
} else {
// Otherwise, wait for the DOMContentLoaded event (generally preferred over 'load')
document.addEventListener('DOMContentLoaded', runInit);
}
})();