eventsTimer

ljovcheg [3191064]

目前為 2025-11-02 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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