NearestEventTimer

ljovcheg [3191064]

目前為 2025-10-28 提交的版本,檢視 最新版本

// ==UserScript==
// @name         NearestEventTimer
// @namespace    NearestEventTimer
// @version      1.0.8
// @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(`
        #eventTimer {
            background-color: rgba(0, 0, 0, 0.5);
            margin-top: 5px;
            margin-bottom: 7px;
            border-radius: 5px;
            padding: 7px;
            overflow: hidden;

            -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;
        }
        #eventTimer:hover{
            background-color: rgba(0, 0, 0, 0.9);
        }
            
    `);

    let apiKey;;
    let events;
    let userEventStartTime;
    let userEventStartTimeUnix;
    let nearestEvent;
    let timeOffset = 0;

    let div;    // timer div object
    let timer;  // holding timeout

    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 {
            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);
        timeOffset = GM_getValue('timeOffset', 0);

        userEventStartTime = GM_getValue('userEventStartTime', null);


        let lastUpdated = GM_getValue('updated', null);

        if (!lastUpdated || currentTimeStamp - lastUpdated > updateInterval || !events || !userEventStartTime) {
            if (apiKey) {
                fetchData();
            } else {
                throwRed("No api key")
                setText("no apy key");
            }
        } else {
            calculate();
        }
    }
    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);
            }

            compareTimestamp(userData);

            throwRed({
                userEventStartTime: userEventStartTime,
                timeOffset: timeOffset
            })
        } else if (userData.error) {
            throwRed(tornData.error);
            setText(userData.error.error);
            return;
        }




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

        calculate();
    }

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

    function calculate() {
        let currentTimeStamp = Math.round(Date.now() / 1000);
        nearestEvent = getNearestEvent(events, currentTimeStamp);
        if (nearestEvent) {
            //userEventStartTimeUnix = setTimeOnUnix(nearestEvent.start + timeOffset, userEventStartTime);
            userEventStartTimeUnix = setTimeOnUnix(nearestEvent.start, userEventStartTime);
            doTimer();
        }
    }
    function doTimer() {

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

        let dif = userEventStartTimeUnix - currentTimeStamp;

        let str = secondsToTime(dif)

        setText(`
            ${nearestEvent.title}<br>
            <b>in: ${str}</b>
        `)

        if (!timer) timer = setInterval(doTimer, 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;

        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 getNearestEvent(events, current_time) {
        let longestWord = ""; // for testing 
        if (!events || events.length === 0) return null;
        let dummyList = []; // no idea why sice doesn't work 
        for (let i = 0; i < events.length; i++) {
            let event = events[i]
            let diff = (event.start - current_time);

            let eventTitle = event.title;
            if (eventTitle.length > longestWord.length) longestWord = eventTitle;
            event.diff = diff;


            if (diff >= 0) {
                dummyList.push(event)
            }
        }
        events = dummyList;
        events.sort(function (a, b) {
            return a.diff - b.diff;
        });

        let nearestEvent = events[0];
        let minDifference = Math.abs(events[0].start - current_time);


        for (let i = 1; i < events.length; i++) {
            const diff = Math.abs(events[i].start - current_time);


            if (diff < minDifference) {
                minDifference = diff;
                nearestEvent = events[i];
            }
        }

        return nearestEvent;
    }

    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(`%cnextEvent${(typeof data !== 'object') ? ': ' + data : ''}`, 'background: #212c37; color: white;padding:10px; border-radius:3px;', (typeof data === 'object') ? data : '');
    }

    function compareTimestamp(data) {
        if (!data.timestamp) return;

        let currentTimeStamp = Math.round(Date.now() / 1000);
        timeOffset = currentTimeStamp - data.timestamp;
        GM_setValue('timeOffset', timeOffset)

    }

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址