Torn Status Monitor

Displays Energy (Green), Nerve (Red), and Happiness (Yellow) with time-to-full timers. GUI is minimizable/draggable. Updates every 2 minutes.

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

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址