Hospital Timer Everywhere

Uses your API key to show a timer for remaining hospital time across all pages with configurable alert time

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Hospital Timer Everywhere
// @namespace    http://tampermonkey.net/
// @license      MIT
// @version      0.3
// @description  Uses your API key to show a timer for remaining hospital time across all pages with configurable alert time
// @author       Weav3r
// @match        https://www.torn.com/*
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const API_URL = 'https://api.torn.com/user/';
    let isRunning = false, isAlertActive = false, alertTime = 300, apiFreq = 60;
    let displayEl, barEl, alertRowEl, countdownTimer, staticTextEl, timerEl;
    let updateTimeout = null;
    let pollingInterval = null;

    const store = {
        get: (k, def = null) => {
            try {
                return JSON.parse(localStorage.getItem(`hospTimer_${k}`)) ?? def;
            } catch {
                return localStorage.getItem(`hospTimer_${k}`) ?? def;
            }
        },
        set: (k, v) => localStorage.setItem(`hospTimer_${k}`, JSON.stringify(v)),
        del: (k) => localStorage.removeItem(`hospTimer_${k}`)
    };

    const showSettings = type => {
        const current = store.get(`${type}Time`, type === 'alert' ? 300 : 60);
        const m = Math.floor(current / 60), s = current % 60;
        const input = prompt(`Set ${type} time (seconds) - Current: ${m ? `${m}m ${s}s` : `${s}s`}`, current);
        if (input !== null) {
            const val = +input;
            if (!isNaN(val) && val >= (type === 'alert' ? 0 : 30)) {
                store.set(`${type}Time`, val);
                if (type === 'alert') {
                    alertTime = val;
                } else {
                    apiFreq = val;
                    if (isRunning) {
                        stopUpdates();
                        startUpdates();
                    }
                }
                alert(`${type} time set to ${val}s`);
            }
        }
    };

    const validateKey = async k => {
        try {
            const r = await fetch(`${API_URL}?key=${k}&comment=HospTimer&selections=basic`);
            const d = await r.json();
            return d.error ? { valid: false, error: d.error.error } : { valid: true, data: d };
        } catch { return { valid: false, error: 'Network error' }; }
    };

    const promptKey = () => {
        const k = prompt('Enter Torn API key:');
        if (k?.trim()) {
            validateKey(k.trim()).then(r => {
                if (r.valid) {
                    store.set('key', k.trim());
                    update();
                    startUpdates();
                } else {
                    alert(`Invalid: ${r.error}`);
                    promptKey();
                }
            });
        }
    };

    const fmt = s => {
        const hours = Math.floor(s/3600);
        const minutes = Math.floor(s%3600/60);

        if (s < 60) {
            return 'less than 1m';
        }

        if (hours > 0) {
            return `${hours}h ${minutes}m`;
        }
        return `${minutes}m`;
    };

    const toggleAlertRow = show => {
        if (show && !alertRowEl) {
            if (!barEl) return;
            alertRowEl = document.createElement('div');
            alertRowEl.id = 'hospital-alert-row';

            const barHeight = barEl.offsetHeight;
            alertRowEl.style.cssText = `width:100%;background:#d32f2f;color:white;padding:6px 0;text-align:center;font-size:13px;font-weight:bold;position:sticky;top:${barHeight}px;z-index:999;`;

            const createBtn = (text, url) => {
                const btn = document.createElement('button');
                btn.textContent = text;
                btn.style.cssText = 'background:#fff;color:#d32f2f;border:none;padding:2px 8px;margin:0 5px;border-radius:3px;cursor:pointer;font-size:11px;';
                btn.onclick = () => window.open(url, '_blank');
                return btn;
            };
            alertRowEl.append('Hospital ending soon! ', createBtn('Your Items', 'https://www.torn.com/item.php'), createBtn('Faction Medical', 'https://www.torn.com/factions.php?step=your#/tab=armoury&start=0&sub=medical'));
            barEl.parentNode.insertBefore(alertRowEl, barEl.nextSibling);
        } else if (!show && alertRowEl) {
            alertRowEl.remove();
            alertRowEl = null;
        }
    };

    const stopCountdown = () => {
        if (countdownTimer) {
            clearTimeout(countdownTimer);
            countdownTimer = null;
        }
    };

    const countdown = (until, details) => {
        if (!displayEl || !barEl) return;
        stopCountdown();
        isAlertActive = false;

        staticTextEl.textContent = `Hospital: ${details} - `;

        const tick = () => {
            const left = Math.max(0, until - Math.floor(Date.now() / 1000));
            if (left > 0) {
                timerEl.textContent = fmt(left);
                const shouldAlert = left <= alertTime;
                if (shouldAlert !== isAlertActive) {
                    isAlertActive = shouldAlert;
                    barEl.style.animation = shouldAlert ? 'hospTimer-flash 1s infinite' : '';
                    toggleAlertRow(shouldAlert);
                }
                countdownTimer = setTimeout(tick, 60000);
            } else {
                staticTextEl.textContent = 'Hospital time completed!';
                timerEl.textContent = '';
                barEl.style.animation = '';
                store.del('status');
                isAlertActive = false;
                toggleAlertRow(false);
                stopUpdates();
                startUpdates();
            }
        };
        tick();
    };

    const stopUpdates = () => {
        isRunning = false;
        if (updateTimeout) {
            clearTimeout(updateTimeout);
            updateTimeout = null;
        }
    };

    const stopPolling = () => {
        if (pollingInterval) {
            clearInterval(pollingInterval);
            pollingInterval = null;
        }
    };

    const cleanup = () => {
        stopUpdates();
        stopCountdown();
        toggleAlertRow(false);
        stopPolling();
    };

    const update = async (force = false) => {
        if (!displayEl) return;
        const key = store.get('key');
        if (!key) {
            staticTextEl.textContent = 'No API key - Click to set';
            timerEl.textContent = '';
            displayEl.style.cursor = 'pointer';
            displayEl.onclick = promptKey;
            return;
        }

        if (!force) {
            const cached = store.get('status');
            if (cached?.status) {
                const { status } = cached;
                if (status.state === 'Hospital') {
                    const left = Math.max(0, status.until - Math.floor(Date.now() / 1000));
                    if (left > 0) {
                        countdown(status.until, status.details);
                        displayEl.style.cursor = 'default';
                        displayEl.onclick = null;
                        return;
                    }
                    store.del('status');
                } else {
                    staticTextEl.textContent = 'Not in hospital';
                    timerEl.textContent = '';
                    displayEl.style.cursor = 'default';
                    displayEl.onclick = null;
                    return;
                }
            }
        }

        try {
            const r = await fetch(`${API_URL}?key=${key}&comment=HospTimer&selections=basic`);
            const d = await r.json();
            store.set('lastApiCall', Math.floor(Date.now() / 1000));
            if (d.error) {
                stopCountdown();
                staticTextEl.textContent = `API Error: ${d.error.error} - Click to fix`;
                timerEl.textContent = '';
                displayEl.style.cursor = 'pointer';
                displayEl.onclick = () => {
                    if (confirm('OK = New key, Cancel = Retry')) {
                        store.del('key');
                        store.del('status');
                        promptKey();
                    } else {
                        update(true);
                    }
                };
                return;
            }
            store.set('status', d);
            const { status } = d;
            if (status?.state === 'Hospital') {
                const left = Math.max(0, status.until - Math.floor(Date.now() / 1000));
                if (left > 0) {
                    countdown(status.until, status.details);
                } else {
                    staticTextEl.textContent = 'Hospital time completed!';
                    timerEl.textContent = '';
                    store.del('status');
                }
            } else {
                staticTextEl.textContent = 'Not in hospital';
                timerEl.textContent = '';
            }
            displayEl.style.cursor = 'default';
            displayEl.onclick = null;
        } catch {
            stopCountdown();
            staticTextEl.textContent = 'Network error - Click to retry';
            timerEl.textContent = '';
            displayEl.style.cursor = 'pointer';
            displayEl.onclick = () => update(true);
        }
    };

    const startUpdates = async () => {
        if (isRunning) return;
        isRunning = true;

        const lastApiCall = store.get('lastApiCall', 0);
        const timeSinceLastCall = Math.floor(Date.now() / 1000) - lastApiCall;

        if (timeSinceLastCall >= apiFreq) {
            await update(true);
        } else {
            await update(false);
        }

        scheduleNextUpdate();
    };

    const scheduleNextUpdate = () => {
        if (!isRunning) return;

        if (updateTimeout) {
            clearTimeout(updateTimeout);
        }

        const lastApiCall = store.get('lastApiCall', 0);
        const timeSinceLastCall = Math.floor(Date.now() / 1000) - lastApiCall;
        const timeUntilNextCall = Math.max(1, apiFreq - timeSinceLastCall) * 1000;

        updateTimeout = setTimeout(async () => {
            if (isRunning) {
                await update(true);
                scheduleNextUpdate();
            }
        }, timeUntilNextCall);
    };

    const init = () => {
        if (barEl) return;
        const wrapper = document.querySelector('.content-wrapper');
        if (!wrapper) return;

        const style = document.createElement('style');
        style.textContent = `@keyframes hospTimer-flash{0%{background-color:#333}50%{background-color:#d32f2f}100%{background-color:#333}}#hospital-timer{border-bottom:2px solid #555!important;box-shadow:0 2px 4px rgba(0,0,0,0.2)}`;
        document.head.appendChild(style);

        barEl = document.createElement('div');
        barEl.id = 'hospital-timer';
        barEl.className = 'cont-gray top-round';
        barEl.style.cssText = 'width:100%;position:sticky;top:0;z-index:1000;margin:0;padding:8px 0;text-align:center;font-size:14px;font-weight:bold';

        displayEl = document.createElement('span');
        displayEl.id = 'timer-display';

        staticTextEl = document.createElement('span');
        timerEl = document.createElement('span');

        displayEl.appendChild(staticTextEl);
        displayEl.appendChild(timerEl);

        staticTextEl.textContent = 'Loading...';

        const createBtn = (svg, pos, title, action) => {
            const btn = document.createElement('span');
            btn.innerHTML = svg;
            btn.style.cssText = `position:absolute;cursor:pointer;opacity:0.7;padding:0;width:14px;height:14px;display:flex;align-items:center;justify-content:center;right:${pos}px;top:50%;transform:translateY(-50%);`;
            btn.title = title;
            btn.onclick = action;
            return btn;
        };

        barEl.append(displayEl,
            createBtn('<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"/></svg>', 28, 'Alert Settings', () => showSettings('alert')),
            createBtn('<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>', 8, 'API Frequency', () => showSettings('api'))
        );
        wrapper.insertBefore(barEl, wrapper.firstChild);

        alertTime = store.get('alertTime', 300);
        apiFreq = store.get('apiTime', 60);
        startUpdates();
    };

    const checkAndInit = () => {
        if (!document.querySelector('#hospital-timer') && document.querySelector('.content-wrapper')) {
            init();
        }
    };

    const startPolling = () => {
        stopPolling();

        checkAndInit();

        pollingInterval = setInterval(checkAndInit, 1000);
    };

    window.addEventListener('beforeunload', cleanup);
    window.addEventListener('pagehide', cleanup);

    startPolling();
})();