eventsTimer

ljovcheg [3191064]

// ==UserScript==
// @name         eventsTimer
// @namespace    NearestEventTimer
// @version      1.0.9
// @grant        GM_getValue
// @grant        GM_setValue
// @description  ljovcheg  [3191064]
// @author       ljovcheg  [3191064] 
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_info
// @license      mit
// ==/UserScript==


(function () {
    'use strict';
    throwRed(`started | v${GM_info.script?.version || null}`);

    GM_addStyle(`
        .currentEvent{
            border: 2px solid rgba(97, 119, 0, 1) !important;
            /*background-color: rgba(85, 255, 0, 0.3) !important;*/
            /*color: rgba(255, 255, 255, 0.9);*/
        }   
        #eventTimer {
            background-color: rgba(0, 0, 0, 0.5);
            margin-top: 5px;
            margin-bottom: 7px;
            border-radius: 5px;
            padding: 7px;
            overflow: hidden;
            cursor: pointer;

            -webkit-touch-callout: none; /* iOS Safari */
            -webkit-user-select: none; /* Safari */
            -khtml-user-select: none; /* Konqueror HTML */
            -moz-user-select: none; /* Old versions of Firefox */
            -ms-user-select: none; /* Internet Explorer/Edge */
            user-select: none; 
            line-height: 16px;
            font-weight: 100;
            font-size: 11px;
            
        }
        #eventTimer:hover{
            background-color: rgba(0, 0, 0, 0.9);
        }
       
    `);

    let apiKey;;
    let events;
    let userEventStartTime;
    let nearestEvent;
    let sortedEvents;

    let tornClockSearchAttempt = 0

    let div;    // timer div object

    //  List of Torn events that uses user start time
    let eventsWithUserTimte = [
        "Valentine's Day",
        "St Patrick's Day",
        "Easter Egg Hunt",
        "420 Day",
        "Museum Day",
        "World Blood Donor Day",
        "World Population Day",
        "World Tiger Day",
        "International Beer Day",
        "Tourism Day",
        "CaffeineCon 2025",
        "Trick or Treat",
        "World Diabetes Day",
        "Slash Wednesday",
        "Christmas town"
    ]

    const updateInterval = 1800; //30min



    function injectDiv() {
        if (document.getElementById("eventTimer")) {
            checkCache();
            return;
        }
        let tornClock = document.querySelector(".tc-clock-tooltip");
        if (tornClock) {
            let p = tornClock.appendChild(document.createElement("div"));
            p.addEventListener("click", divClicked);
            p.innerHTML = `<div id="eventTimer">...</div>`;
            div = document.getElementById('eventTimer');
            checkCache();
        } else {
            //  lets try again 

            if (tornClockSearchAttempt < 3) {
                tornClockSearchAttempt++;
                throwRed(`.tc-clock-tooltip not found, ${tornClockSearchAttempt} try`);
                setTimeout(() => {
                    injectDiv();
                }, 300);
            } else {
                throwRed(`.tc-clock-tooltip not found :(`);
            }

        }
    }
    injectDiv();
    function checkCache() {
        let currentTimeStamp = Math.round(Date.now() / 1000);

        //  read cache
        apiKey = GM_getValue('timer_api_key', null);
        events = GM_getValue('events', null);

        userEventStartTime = GM_getValue('userEventStartTime', null);


        let lastUpdated = GM_getValue('updated', null);
        if (!apiKey) {
            throwRed("No api key")
            setText("limited apy key needed");
            return;
        }
        if (!lastUpdated || currentTimeStamp - lastUpdated > updateInterval || !events || !userEventStartTime) {
            fetchData();
        } else {
            setEventTimes("cache");
        }
    }
    async function fetchData() {
        setText("fetching torn...");
        const tornData = await GM_fetch('torn', 'calendar');
        if (tornData.calendar) {
            let json = tornData.calendar;
            if (json.events && json.competitions) {
                events = json["events"].concat(json.competitions);
            } else {
                events = json.events;
            }
            GM_setValue('events', events)
        } else if (tornData.error) {
            throwRed(tornData.error);
            setText(tornData.error.error);
            return;
        }

        setText("fetching user...");
        const userData = await GM_fetch('user', 'calendar,timestamp');
        if (userData.calendar) {
            let json = userData.calendar;
            if (json.start_time) {
                userEventStartTime = json.start_time.toLowerCase().split(" tct")[0];
                GM_setValue('userEventStartTime', userEventStartTime);
            }
        } else if (userData.error) {
            throwRed(tornData.error);
            setText(userData.error.error);
            return;
        }




        let currentTimeStamp = Math.round(Date.now() / 1000);
        GM_setValue('updated', currentTimeStamp);

        setEventTimes("fetch");
    }



    function setEventTimes(from = null) {
        //if (from) throwRed(`Came here from ${from}`);

        // setting evetns start/end times with user start time if needed

        /*
        events.push({
            title: "Test event",
            start: 1761999300,
            end: 1762172100
        })
            */


        events.forEach(event => {
            let eventStartTime = event.start;
            let eventEndTime = event.end;
            let isEventWithUserTime = (eventsWithUserTimte.indexOf(event.title) !== -1) ? true : false;

            if (isEventWithUserTime) {
                eventStartTime = setTimeOnUnix(event.start, userEventStartTime);
                eventEndTime = setTimeOnUnix(event.end, userEventStartTime);
                event.userTimeAffect = true;
            }

            event.start = eventStartTime;
            event.end = eventEndTime;

        });

        getNearestEvent();

    }

    function getNearestEvent() {
        let currentTimeStamp = Math.round(Date.now() / 1000);

        if (!events || events.length === 0) return null;



        let eventsList = [];
        events.forEach(event => {
            event.startDiff = (event.start - currentTimeStamp);
            event.endDiff = (event.end - currentTimeStamp);

            if (event.startDiff >= 0 || event.endDiff >= 0) eventsList.push(event)
            //console.log(event.title, event.startDiff, event.endDiff)
        });

        const now = Math.floor(Date.now() / 1000); // current timestamp in seconds

        const upcomingOrActiveEvents = events.filter(e => !(e.startDiff < 0 && e.endDiff < 0));

        // Optional: sort after filtering (same logic as before)
        sortedEvents = upcomingOrActiveEvents.sort((a, b) => {
            const aActive = a.startDiff <= 0 && a.endDiff >= 0;
            const bActive = b.startDiff <= 0 && b.endDiff >= 0;

            if (aActive && !bActive) return -1;
            if (!aActive && bActive) return 1;

            if (aActive && bActive) return a.endDiff - b.endDiff;
            if (a.startDiff > 0 && b.startDiff > 0) return a.startDiff - b.startDiff;

            return a.startDiff - b.startDiff;
        });


        // Active events (sorted by soonest ending)
        const activeEvents = sortedEvents.filter(e => now >= e.start && now <= e.end);


        //console.log("Active events:", activeEvents);
        //console.log("Sorted events:", sortedEvents);

        nearestEvent = sortedEvents[0];

        showTimer();
        // console.log(events);
        // console.log(eventsList)
    }
    function showTimer() {
        let currentTimeStamp = Math.round(Date.now() / 1000);
        let difTime = 0;
        let val = "in";
        let currentEvent = false;
        if (nearestEvent.startDiff > 0) {
            difTime = nearestEvent.start - currentTimeStamp;
            if (div.classList.contains('currentEvent')) {
                div.classList.remove("currentEvent")
            }
        } else {
            if (!div.classList.contains('currentEvent')) {
                div.classList.add("currentEvent")
            }
            difTime = nearestEvent.end - currentTimeStamp;
            val = "ends";
            currentEvent = true;
        }

        let text = `
            ${nearestEvent.title}<br>
            <b>${val}: ${secondsToTime(difTime)}</b>
        `;
        /*
        if (currentEvent && sortedEvents[1].startDiff > 0) {
            let nextDiff = sortedEvents[1].start - currentTimeStamp;;
            text += `
            
                <br/>${sortedEvents[1].title}<br/>
                <b>in: ${secondsToTime(nextDiff)}</b>
            `;
        }
            */
        setText(text)

        if (difTime < 0) {
            throwRed(`Event ${nearestEvent.title} is over.`)
            getNearestEvent();
            return;
        }

        setTimeout(showTimer, 1000);
    }

    function secondsToTime(totalSeconds) {
        const days = Math.floor(totalSeconds / 86400); // 1 day = 86400 seconds
        totalSeconds %= 86400;

        const hours = Math.floor(totalSeconds / 3600);
        totalSeconds %= 3600;

        const minutes = Math.floor(totalSeconds / 60);
        const seconds = totalSeconds % 60;

        // Pad with leading zeros
        const paddedHours = hours < 10 ? "0" + hours : hours;
        const paddedMinutes = minutes < 10 ? "0" + minutes : minutes;
        const paddedSeconds = seconds < 10 ? "0" + seconds : seconds;

        if (days > 0) {
            // Include days, show full time
            return `${days} day${days > 1 ? "s" : ""} ${paddedHours}:${paddedMinutes}:${paddedSeconds}`;
        } else if (hours > 0) {
            // Hours, minutes, seconds
            return `${paddedHours}:${paddedMinutes}:${paddedSeconds}`;
        } else if (minutes > 0) {
            // Only minutes and seconds
            return `${paddedMinutes} min ${paddedSeconds} sec`;
        } else {
            // Only seconds
            return `${paddedSeconds} sec`;
        }
    }

    function secondsToTime_old(totalSeconds) {
        const days = Math.floor(totalSeconds / 86400); // 1 day = 86400 seconds
        totalSeconds %= 86400;

        const hours = Math.floor(totalSeconds / 3600);
        totalSeconds %= 3600;

        const minutes = Math.floor(totalSeconds / 60);
        const seconds = totalSeconds % 60;

        const paddedHours = hours < 10 ? "0" + hours : hours;
        const paddedMinutes = minutes < 10 ? "0" + minutes : minutes;
        const paddedSeconds = seconds < 10 ? "0" + seconds : seconds;

        if (days > 0) {
            // Include days and limit hours to 0–23
            return `${days} day${days > 1 ? "s" : ""} ${paddedHours}:${paddedMinutes}:${paddedSeconds}`;
        } else {
            // No days, just hours/minutes/seconds
            return `${paddedHours}:${paddedMinutes}:${paddedSeconds}`;
        }
    }

    function setTimeOnUnix(unixTime, timeString) {

        const [targetHour, targetMinute] = timeString.split(":").map(Number);


        const date = new Date(unixTime * 1000);


        date.setUTCHours(targetHour);
        date.setUTCMinutes(targetMinute);
        date.setUTCSeconds(0);
        date.setUTCMilliseconds(0);



        return Math.floor(date.getTime() / 1000);

    }

    function divClicked() {
        if (apiKey === null) apiKey = '';
        let w = prompt("Api key", apiKey);
        if (w || w === "" && w !== null) {
            //save key
            GM_setValue('timer_api_key', w);
            apiKey = w;
        }
        if (apiKey && w !== null) fetchData();

    }
    function setText(data) {
        div.innerHTML = data
    }
    function throwRed(data) {
        console.log(`%ceventsTimer${(typeof data !== 'object') ? ': ' + data : ''}`, 'background: #212c37; color: white;padding:10px; border-radius:3px;', (typeof data === 'object') ? data : '');
    }

    async function GM_fetch(page, selections) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://api.torn.com/v2/${page}/?selections=${selections}&key=${apiKey}`,
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                },
                onload: function (response) {
                    try {
                        if (!response || !response.responseText) {
                            return reject(new Error("Empty response"));
                        }
                        const json = JSON.parse(response.responseText);
                        resolve(json);

                    } catch (err) {
                        reject(err);
                    }
                },
                onerror: function (err) {
                    reject(err);
                },
            });
        });
    }
})();