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