Hospital Timer Everywhere

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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