Torn City Virus Coding Alert

Shows virus coding status once daily when active, or every hour when no virus is being programmed or when virus coding is complete

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn City Virus Coding Alert
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Shows virus coding status once daily when active, or every hour when no virus is being programmed or when virus coding is complete
// @author       Woeka
// @license      MIT
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    // ===== CONFIGURATION =====
    // API key is stored via Tampermonkey menu (right-click Tampermonkey icon → Script name → Set Api Key)
    // Or you can set it manually below and it will be saved automatically
    const API_KEY_STORAGE_KEY = 'torn_virus_alert_apikey';
    let API_KEY = GM_getValue(API_KEY_STORAGE_KEY, 'YOUR_API_KEY_HERE');
    
    // Check interval in milliseconds (default: 1 hour = 3600000ms)
    const CHECK_INTERVAL = 60 * 60 * 1000;

    // Storage key for last alert time (to prevent spam)
    const LAST_ALERT_KEY = 'torn_virus_last_alert';
    const LAST_NO_VIRUS_ALERT_KEY = 'torn_virus_last_no_virus_alert';

    const PC_LINK = 'https://www.torn.com/pc.php';
    
    // Register Tampermonkey menu command to set API key
    GM_registerMenuCommand('Set Api Key', function() {
        setApiKey();
    });
    
    function setApiKey() {
        const currentKey = GM_getValue(API_KEY_STORAGE_KEY, '');
        const userInput = prompt(
            'Please enter your Torn City API Key (16 characters):\n\nGet your API key from: https://www.torn.com/preferences.php#tab=api',
            currentKey || ''
        );
        
        if (userInput !== null && userInput.trim() !== '') {
            if (userInput.length === 16) {
                GM_setValue(API_KEY_STORAGE_KEY, userInput.trim());
                API_KEY = userInput.trim();
                alert('API key saved successfully!');
                // Refresh the check
                checkVirusStatus();
            } else {
                alert('Invalid API key length. API keys must be exactly 16 characters.');
            }
        }
    }
    
    // Track active notifications to prevent duplicates
    let activeNotificationId = null;
    let notificationTimeout = null;

    function removeExistingNotifications() {
        // Remove any existing notifications
        const existing = document.querySelectorAll('[id^="torn-virus-notification-"]');
        existing.forEach(notif => {
            notif.remove();
        });
        activeNotificationId = null;
        if (notificationTimeout) {
            clearTimeout(notificationTimeout);
            notificationTimeout = null;
        }
    }

    function showNotification(virusName, timeLeft, daysRemaining, hoursRemaining, isTestMode = false, noApiKey = false, virusDone = false) {
        // Don't show notification on pc.php page
        if (window.location.pathname === '/pc.php' || window.location.href.includes('/pc.php')) {
            console.log('Torn City Virus: Skipping notification on pc.php page');
            return;
        }
        
        console.log('Torn City Virus: showNotification called with:', { virusName, timeLeft, daysRemaining, hoursRemaining, isTestMode });
        
        // Remove any existing notifications first
        removeExistingNotifications();
        
        // Wait for body to be available
        const addNotification = () => {
            console.log('Torn City Virus: addNotification called, document.body exists:', !!document.body);
            if (!document.body) {
                console.log('Torn City Virus: Waiting for document.body...');
                setTimeout(addNotification, 100);
                return;
            }
            
            console.log('Torn City Virus: Creating notification element...');

            // Create a custom notification with clickable link
            const notificationId = 'torn-virus-notification-' + Date.now();
            activeNotificationId = notificationId;
            const notification = document.createElement('div');
            notification.id = notificationId;
            notification.style.cssText = `
                position: fixed;
                top: 20px;
                right: 20px;
                background: #1a1a1a;
                color: #e0e0e0;
                padding: 20px;
                border-radius: 4px;
                box-shadow: 0 2px 8px rgba(0,0,0,0.5);
                z-index: 999999;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
                max-width: 400px;
                border: 1px solid #d32f2f;
            `;
            
            let title, message;
            if (noApiKey) {
                title = '⚠️ Torn City Virus Alert - No API Key';
                message = 'No API key found. Please set your API key using the Tampermonkey menu:<br><br>Right-click the Tampermonkey icon → Torn City Virus Coding Alert → Set Api Key<br><br>Or get your API key from: <a href="https://www.torn.com/preferences.php#tab=api" style="color: #5a9eff; text-decoration: underline;">Torn City Preferences → API</a>';
            } else if (virusDone) {
                title = '✅ Torn City Virus Alert';
                message = `Virus coding is complete!<br><br>Your ${virusName} has finished coding.`;
            } else if (virusName === '' && timeLeft === 0) {
                title = '⚠️ Torn City Virus Alert';
                message = 'No active virus coding in progress.';
            } else {
                title = '⚠️ Torn City Virus Alert!';
                message = `Virus: ${virusName}<br>Time remaining: ${formatTime(timeLeft)}<br>(${daysRemaining}d ${hoursRemaining}h)`;
            }
            
            const closeBtn = document.createElement('button');
            closeBtn.textContent = 'Close';
            closeBtn.type = 'button'; // Prevent form submission if inside a form
            closeBtn.style.cssText = `
                float: right;
                background: #333333;
                color: #e0e0e0;
                border: 1px solid #555555;
                padding: 8px 16px;
                border-radius: 3px;
                cursor: pointer;
                font-weight: normal;
                margin-left: 10px;
                transition: background 0.2s;
            `;
            closeBtn.onmouseover = function() { this.style.background = '#444444'; };
            closeBtn.onmouseout = function() { this.style.background = '#333333'; };
            // Use a more robust event handler
            const closeHandler = function(e) {
                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation();
                console.log('Torn City Virus: Close button clicked');
                removeExistingNotifications();
            };
            closeBtn.addEventListener('click', closeHandler, true);
            // Also handle mousedown as fallback
            closeBtn.addEventListener('mousedown', closeHandler, true);
            
            const pcLink = document.createElement('a');
            pcLink.href = PC_LINK;
            pcLink.textContent = 'Open PC Page';
            pcLink.style.cssText = `
                display: inline-block;
                background: #2d5aa0;
                color: #ffffff;
                padding: 8px 16px;
                text-decoration: none;
                border-radius: 3px;
                font-weight: normal;
                border: 1px solid #3d6ab0;
                transition: background 0.2s;
            `;
            pcLink.onmouseover = function() { this.style.background = '#3d6ab0'; };
            pcLink.onmouseout = function() { this.style.background = '#2d5aa0'; };
            
            const titleDiv = document.createElement('div');
            titleDiv.style.cssText = 'font-weight: 600; margin-bottom: 12px; font-size: 15px; color: #ffffff;';
            titleDiv.textContent = title;
            
            const messageDiv = document.createElement('div');
            messageDiv.style.cssText = 'margin-bottom: 15px; line-height: 1.6; color: #d0d0d0; font-size: 14px;';
            messageDiv.innerHTML = message;
            
            const buttonContainer = document.createElement('div');
            buttonContainer.style.cssText = 'margin-top: 10px;';
            buttonContainer.appendChild(pcLink);
            buttonContainer.appendChild(closeBtn);
            
            notification.appendChild(titleDiv);
            notification.appendChild(messageDiv);
            notification.appendChild(buttonContainer);
            
            console.log('Torn City Virus: Appending notification to body...');
            document.body.appendChild(notification);
            console.log('Torn City Virus: Notification displayed! Element:', notification);
            console.log('Torn City Virus: Notification parent:', notification.parentElement);
            console.log('Torn City Virus: Notification computed style:', window.getComputedStyle(notification).display);
            
            // Auto-remove after 10 seconds (or keep it until manually closed)
            notificationTimeout = setTimeout(() => {
                if (notification.parentElement && notification.id === activeNotificationId) {
                    notification.remove();
                    activeNotificationId = null;
                }
            }, 10000);
        };
        
        addNotification();
    }

    function checkVirusStatus() {
        // Don't check on pc.php page
        if (window.location.pathname === '/pc.php' || window.location.href.includes('/pc.php')) {
            console.log('Torn City Virus: Skipping check on pc.php page');
            return;
        }
        
        console.log('Torn City Virus: checkVirusStatus called');
        
        // Get the latest API key from storage
        API_KEY = GM_getValue(API_KEY_STORAGE_KEY, 'YOUR_API_KEY_HERE');
        
        if (API_KEY === 'YOUR_API_KEY_HERE' || !API_KEY || API_KEY.length !== 16) {
            console.warn('Torn City Virus Alert: Please set your API_KEY using the Tampermonkey menu!');
            // Show notification that no API key was found
            showNotification('', 0, 0, 0, false, true);
            return;
        }
        
        console.log('Torn City Virus: Making API request...');

        GM_xmlhttpRequest({
            method: 'GET',
            url: 'https://api.torn.com/v2/user/virus',
            headers: {
                'accept': 'application/json',
                'Authorization': `ApiKey ${API_KEY}`
            },
            onload: function(response) {
                console.log('Torn City Virus: API response received, status:', response.status);
                try {
                    if (response.status !== 200) {
                        console.error('Torn City API Error: HTTP', response.status, response.responseText);
                        return;
                    }

                    const data = JSON.parse(response.responseText);
                    console.log('Torn City Virus: API data:', data);
                    
                    // Check for API errors
                    if (data.error) {
                        console.error('Torn City API Error:', data.error);
                        return;
                    }

                    // Check if virus data exists
                    if (!data.virus || !data.virus.until) {
                        console.log('Torn City Virus: No active virus coding in progress');
                        console.log('Available data:', Object.keys(data));
                        // Show notification every hour when no virus is being programmed
                        const lastNoVirusAlertTime = GM_getValue(LAST_NO_VIRUS_ALERT_KEY, 0);
                        const now = Date.now();
                        const oneHourAgo = now - (60 * 60 * 1000);

                        if (lastNoVirusAlertTime < oneHourAgo) {
                            console.log('Torn City Virus: Showing no virus notification (once per hour)');
                            showNotification('', 0, 0, 0, false, false, false);
                            GM_setValue(LAST_NO_VIRUS_ALERT_KEY, now);
                        } else {
                            console.log('Torn City Virus: No virus notification already shown this hour, skipping');
                        }
                        return;
                    }
                    
                    console.log('Torn City Virus: Virus data found:', data.virus);

                    const virusCompletionTime = data.virus.until;
                    const virusName = data.virus.item ? data.virus.item.name : 'virus';

                    const currentTime = Math.floor(Date.now() / 1000);
                    const timeLeft = virusCompletionTime - currentTime;
                    const daysLeft = timeLeft / (24 * 60 * 60);

                    const daysRemaining = Math.floor(daysLeft);
                    const hoursRemaining = Math.floor((timeLeft % (24 * 60 * 60)) / 3600);
                    const minutesRemaining = Math.floor((timeLeft % 3600) / 60);
                    
                    console.log(`Torn City Virus (${virusName}): ${daysLeft.toFixed(2)} days remaining`);

                    // Show notification once per day when virus is active, or when virus is done
                    if (timeLeft > 0) {
                        // Prevent alert spam - only alert once per day
                        const lastAlertTime = GM_getValue(LAST_ALERT_KEY, 0);
                        const now = Date.now();
                        const oneDayAgo = now - (24 * 60 * 60 * 1000);

                        if (lastAlertTime < oneDayAgo) {
                            console.log('Torn City Virus: Showing alert notification (once per day)');
                            showNotification(virusName, timeLeft, daysRemaining, hoursRemaining, false, false, false);
                            GM_setValue(LAST_ALERT_KEY, now);
                        } else {
                            console.log('Torn City Virus: Alert already shown today, skipping');
                        }
                    } else {
                        console.log(`Torn City Virus (${virusName}): Coding is complete!`);
                        // Show notification when virus is done (once per hour)
                        const lastNoVirusAlertTime = GM_getValue(LAST_NO_VIRUS_ALERT_KEY, 0);
                        const now = Date.now();
                        const oneHourAgo = now - (60 * 60 * 1000);

                        if (lastNoVirusAlertTime < oneHourAgo) {
                            console.log('Torn City Virus: Showing virus done notification (once per hour)');
                            showNotification(virusName, 0, 0, 0, false, false, true);
                            GM_setValue(LAST_NO_VIRUS_ALERT_KEY, now);
                        }
                    }
                } catch (error) {
                    console.error('Torn City Virus Alert: Error parsing response', error);
                }
            },
            onerror: function(error) {
                console.error('Torn City Virus Alert: Network error', error);
            }
        });
    }

    function formatTime(seconds) {
        const days = Math.floor(seconds / (24 * 60 * 60));
        const hours = Math.floor((seconds % (24 * 60 * 60)) / 3600);
        const minutes = Math.floor((seconds % 3600) / 60);
        
        let result = '';
        if (days > 0) result += `${days}d `;
        if (hours > 0) result += `${hours}h `;
        if (minutes > 0) result += `${minutes}m`;
        
        return result.trim() || 'Less than a minute';
    }

    // Test notification immediately to verify it works (but not on pc.php)
    // Commented out to prevent duplicate notifications - uncomment for testing
    // console.log('Torn City Virus: Script loaded, testing notification...');
    // setTimeout(() => {
    //     if (window.location.pathname !== '/pc.php' && !window.location.href.includes('/pc.php')) {
    //         console.log('Torn City Virus: Testing notification function...');
    //         showNotification('Test Virus', 604800, 7, 0, true);
    //     }
    // }, 1000);
    
    // Initial check after a short delay (to let page load)
    setTimeout(checkVirusStatus, 2000);
    
    // Set up periodic checks
    setInterval(checkVirusStatus, CHECK_INTERVAL);
    
    console.log('Torn City Virus Alert script loaded. Checking every', CHECK_INTERVAL / 1000 / 60, 'minutes.');
})();