Adds a custom icon to Torn's sidebar status icons
// ==UserScript==
// @name Virus Status
// @namespace https://www.torn.com/
// @version 1.0.0
// @description Adds a custom icon to Torn's sidebar status icons
// @author Cypher[2641265]
// @match https://www.torn.com/*
// @run-at document-end
// @connect api.torn.com
// @grant GM_xmlhttpRequest
// @license MIT
// ==/UserScript==
(function () {
"use strict";
const CUSTOM_ICON_ID = "custom-virus-status-icon";
const CUSTOM_ICON_CLASS = "custom-virus-status-slot";
const PROGRAMMING_CENTER_URL = "https://www.torn.com/pc.php";
const API_KEY_STORAGE_KEY = "torn_minimal_key";
const API_REFRESH_INTERVAL_MS = 60000;
const TOOLTIP_REFRESH_INTERVAL_MS = 1000;
const VIRUS_ICON_ACTIVE_URL = "https://i.ibb.co/pBQKT8jF/Virus-Green.png";
const VIRUS_ICON_IDLE_URL = "https://i.ibb.co/KxmNXG2v/Virus-Red.png";
const ICON_STATE_STORAGE_KEY = "virus-status-icon-state";
let apiRefreshTimer = null;
let tooltipRefreshTimer = null;
let latestVirusPayload = null;
let latestTooltipLabel = "API Key not set";
let tooltipElement = null;
function getApiKey() {
try {
return (window.localStorage.getItem(API_KEY_STORAGE_KEY) || "").trim();
} catch (error) {
return "";
}
}
function isApiKeySet() {
return Boolean(getApiKey());
}
function ensureApiKeyStorageKeyExists() {
try {
if (window.localStorage.getItem(API_KEY_STORAGE_KEY) === null) {
window.localStorage.setItem(API_KEY_STORAGE_KEY, "");
}
} catch (error) {
// Ignore storage failures; users can still paste key during this session.
}
}
function setApiKey(apiKey) {
const normalized = String(apiKey || "").trim();
try {
window.localStorage.setItem(API_KEY_STORAGE_KEY, normalized);
} catch (error) {
// Ignore storage failures; runtime key will still be used for this page.
}
}
function showApiKeyPopup(onSubmit) {
const existingPopup = document.getElementById("virus-api-popup");
if (existingPopup) existingPopup.remove();
const popup = document.createElement("div");
popup.id = "virus-api-popup";
popup.style.position = "fixed";
popup.style.top = "50%";
popup.style.left = "50%";
popup.style.transform = "translate(-50%, -50%)";
popup.style.background = "#222";
popup.style.color = "#fff";
popup.style.padding = "24px 18px 18px 18px";
popup.style.borderRadius = "8px";
popup.style.boxShadow = "0 2px 16px #000a";
popup.style.zIndex = "999999";
popup.style.minWidth = "320px";
popup.style.textAlign = "center";
popup.innerHTML = `
<div style="font-size:1.1em;margin-bottom:10px;">Enter Minimal API Key</div>
<input id="virus-api-key-input" type="text" placeholder="API Key" style="width:90%;padding:6px;margin-bottom:10px;border-radius:4px;border:1px solid #444;background:#111;color:#fff;">
<br>
<button id="virus-api-key-save" style="padding:6px 18px;border-radius:4px;border:none;background:#4caf50;color:#fff;font-weight:bold;cursor:pointer;">Save</button>
`;
document.body.appendChild(popup);
const input = document.getElementById("virus-api-key-input");
const saveButton = document.getElementById("virus-api-key-save");
saveButton.onclick = function () {
const value = input.value.trim();
if (!value) return;
onSubmit(value);
popup.remove();
};
input.onkeydown = function (event) {
if (event.key === "Enter") {
saveButton.click();
}
};
input.focus();
}
function addStyles() {
if (document.getElementById("custom-virus-status-styles")) return;
const style = document.createElement("style");
style.id = "custom-virus-status-styles";
style.textContent = `
li#${CUSTOM_ICON_ID} {
list-style: none !important;
background: none !important;
background-image: none !important;
}
#custom-virus-status-tooltip {
position: fixed;
z-index: 999999;
display: none;
padding: 7px 9px;
border: 1px solid #2d2d2d;
border-radius: 4px;
background: #4a4a4a;
background-image: linear-gradient(to bottom, #525252, #3f3f3f);
color: #f2f2f2;
font-size: 12px;
line-height: 1.25;
white-space: pre-line;
pointer-events: none;
}
#custom-virus-status-tooltip.is-visible {
display: block;
}
li#${CUSTOM_ICON_ID}::marker,
li#${CUSTOM_ICON_ID}::before,
li#${CUSTOM_ICON_ID}::after,
li#${CUSTOM_ICON_ID} > a::before,
li#${CUSTOM_ICON_ID} > a::after {
content: none !important;
display: none !important;
}
li#${CUSTOM_ICON_ID} > a {
position: relative !important;
display: flex !important;
align-items: center;
justify-content: center;
background: none !important;
background-image: none !important;
text-decoration: none;
overflow: visible;
}
li#${CUSTOM_ICON_ID} > a img {
display: block;
width: 16px;
height: 16px;
object-fit: contain;
pointer-events: none;
transform: translateY(-1px);
}
li#${CUSTOM_ICON_ID} > a:hover {
filter: brightness(1.15);
}
`;
document.head.appendChild(style);
}
function findStatusIconList() {
return document.querySelector('ul[class*="status-icons___"]');
}
function getIconLink() {
return document.querySelector(`li#${CUSTOM_ICON_ID} > a`);
}
function ensureTooltipElement() {
if (tooltipElement && document.body.contains(tooltipElement)) {
return tooltipElement;
}
tooltipElement = document.createElement("div");
tooltipElement.id = "custom-virus-status-tooltip";
tooltipElement.setAttribute("role", "tooltip");
document.body.appendChild(tooltipElement);
return tooltipElement;
}
function positionTooltip() {
const link = getIconLink();
const tooltip = ensureTooltipElement();
if (!link || !tooltip.classList.contains("is-visible")) return;
const linkRect = link.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
const top = Math.max(8, linkRect.top - tooltipRect.height - 8);
const left = Math.min(
window.innerWidth - tooltipRect.width - 8,
Math.max(8, linkRect.left + (linkRect.width - tooltipRect.width) / 2),
);
tooltip.style.top = `${top}px`;
tooltip.style.left = `${left}px`;
}
function showTooltip() {
const tooltip = ensureTooltipElement();
tooltip.textContent = latestTooltipLabel;
tooltip.classList.add("is-visible");
positionTooltip();
}
function hideTooltip() {
const tooltip = ensureTooltipElement();
tooltip.classList.remove("is-visible");
}
function getNativeTooltipContent(link = getIconLink()) {
if (!link) return null;
const tooltipId = link.getAttribute("aria-describedby");
if (!tooltipId) return null;
return document.querySelector(`#${tooltipId} .ui-tooltip-content`);
}
function parseTimestamp(value) {
if (value === null || value === undefined || value === "") return null;
let timestamp = null;
if (typeof value === "number" && Number.isFinite(value)) {
timestamp = value < 1e12 ? value * 1000 : value;
} else if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed) return null;
if (/^\d+$/.test(trimmed)) {
const numericValue = Number(trimmed);
timestamp = numericValue < 1e12 ? numericValue * 1000 : numericValue;
} else {
const parsedValue = Date.parse(trimmed);
if (!Number.isNaN(parsedValue)) {
timestamp = parsedValue;
}
}
}
return timestamp;
}
function formatDurationRemaining(until) {
const timestamp = parseTimestamp(until);
if (timestamp === null) return "Unknown";
const remainingMs = Math.max(0, timestamp - Date.now());
const totalSeconds = Math.floor(remainingMs / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const parts = [
`${days} ${days === 1 ? "day" : "days"}`,
`${hours} ${hours === 1 ? "hour" : "hours"}`,
`${minutes} ${minutes === 1 ? "minute" : "minutes"}`,
`${seconds} ${seconds === 1 ? "second" : "seconds"}`,
];
return `${parts[0]}, ${parts[1]}, ${parts[2]} and ${parts[3]}`;
}
function getVirusPayload(response) {
if (!response || typeof response !== "object") return null;
if (response.name !== undefined || response.until !== undefined) {
return response;
}
if (response.virus && typeof response.virus === "object") {
return {
name: response.virus.item?.name,
until: response.virus.until,
};
}
if (response.data && typeof response.data === "object") {
if (
response.data.name !== undefined ||
response.data.until !== undefined
) {
return response.data;
}
if (response.data.virus && typeof response.data.virus === "object") {
return {
name: response.data.virus.item?.name,
until: response.data.virus.until,
};
}
}
return null;
}
function buildTooltipLabel(payload) {
if (!payload) {
return "Virus Timer: No active virus programming";
}
const name = payload.name ? String(payload.name) : "Unknown";
const remaining = formatDurationRemaining(payload.until);
return `Planning: ${name}\n${remaining}`;
}
function updateIconTooltip(label) {
latestTooltipLabel = label;
const link = getIconLink();
if (!link) return;
link.setAttribute("aria-label", label);
const tooltip = ensureTooltipElement();
if (tooltip.classList.contains("is-visible")) {
tooltip.textContent = label;
positionTooltip();
}
}
function syncIconAriaLabel() {
const link = getIconLink();
if (!link) return;
link.setAttribute("aria-label", latestTooltipLabel);
}
function getStoredIconState() {
try {
const storedState = window.localStorage.getItem(ICON_STATE_STORAGE_KEY);
return storedState === "idle" ? "idle" : "active";
} catch (error) {
return "active";
}
}
function setStoredIconState(payload) {
const state = payload ? "active" : "idle";
try {
window.localStorage.setItem(ICON_STATE_STORAGE_KEY, state);
} catch (error) {
// Ignore storage failures; icon still updates for current session.
}
return state;
}
function getIconUrlForState(state) {
return state === "idle" ? VIRUS_ICON_IDLE_URL : VIRUS_ICON_ACTIVE_URL;
}
function updateIconImage(payload) {
const link = getIconLink();
if (!link) return;
const iconImage = link.querySelector("img");
if (!iconImage) return;
const state = setStoredIconState(payload);
iconImage.src = getIconUrlForState(state);
}
function requestVirusStatus() {
const apiKey = getApiKey();
if (!apiKey) {
return Promise.reject(new Error("API key not set"));
}
const virusApiUrl = `https://api.torn.com/v2/user/virus?key=${apiKey}`;
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: virusApiUrl,
headers: {
Accept: "application/json",
},
onload: function (response) {
if (response.status < 200 || response.status >= 300) {
reject(
new Error(
`Virus API request failed with status ${response.status}`,
),
);
return;
}
try {
resolve(JSON.parse(response.responseText));
} catch (error) {
reject(new Error("Virus API returned invalid JSON"));
}
},
onerror: function () {
reject(new Error("Virus API request failed"));
},
});
});
}
async function refreshVirusStatus() {
if (!isApiKeySet()) {
latestVirusPayload = null;
updateIconImage(null);
updateIconTooltip("API Key not set");
return;
}
try {
const response = await requestVirusStatus();
latestVirusPayload = getVirusPayload(response);
updateIconImage(latestVirusPayload);
updateIconTooltip(buildTooltipLabel(latestVirusPayload));
} catch (error) {
latestVirusPayload = null;
updateIconTooltip("Virus Timer: Unable to load virus status");
console.error("Virus Status userscript", error);
}
}
function refreshTooltipCountdown() {
if (!latestVirusPayload) return;
updateIconTooltip(buildTooltipLabel(latestVirusPayload));
}
function createCustomIcon(statusList) {
const templateItem = statusList.querySelector("li");
const li = templateItem
? templateItem.cloneNode(true)
: document.createElement("li");
li.id = CUSTOM_ICON_ID;
li.className = [templateItem?.className || "", CUSTOM_ICON_CLASS]
.filter(Boolean)
.join(" ");
let link = li.querySelector("a");
if (!link) {
link = document.createElement("a");
li.appendChild(link);
}
link.href = PROGRAMMING_CENTER_URL;
link.setAttribute("aria-label", latestTooltipLabel);
link.setAttribute("tabindex", "0");
link.setAttribute("data-is-tooltip-opened", "false");
link.removeAttribute("title");
link.replaceChildren();
const iconImage = document.createElement("img");
iconImage.src = getIconUrlForState(getStoredIconState());
iconImage.alt = "";
iconImage.setAttribute("aria-hidden", "true");
iconImage.decoding = "async";
link.appendChild(iconImage);
link.addEventListener("click", function (event) {
event.preventDefault();
if (!isApiKeySet()) {
showApiKeyPopup(function (enteredKey) {
setApiKey(enteredKey);
latestTooltipLabel = "Virus Timer: Loading...";
updateIconTooltip(latestTooltipLabel);
refreshVirusStatus();
});
return;
}
window.location.href = PROGRAMMING_CENTER_URL;
});
link.addEventListener("mouseenter", showTooltip);
link.addEventListener("mouseleave", hideTooltip);
link.addEventListener("focus", showTooltip);
link.addEventListener("blur", hideTooltip);
link.addEventListener("mouseleave", syncIconAriaLabel);
link.addEventListener("blur", syncIconAriaLabel);
return li;
}
function injectCustomIcon() {
const statusList = findStatusIconList();
if (!statusList) return;
const existing = document.getElementById(CUSTOM_ICON_ID);
if (existing) {
if (existing.parentElement !== statusList) {
statusList.appendChild(existing);
}
return;
}
statusList.appendChild(createCustomIcon(statusList));
}
function init() {
ensureApiKeyStorageKeyExists();
addStyles();
injectCustomIcon();
refreshVirusStatus();
if (apiRefreshTimer === null) {
apiRefreshTimer = window.setInterval(
refreshVirusStatus,
API_REFRESH_INTERVAL_MS,
);
}
if (tooltipRefreshTimer === null) {
tooltipRefreshTimer = window.setInterval(
refreshTooltipCountdown,
TOOLTIP_REFRESH_INTERVAL_MS,
);
}
window.addEventListener("scroll", positionTooltip, true);
window.addEventListener("resize", positionTooltip);
const observer = new MutationObserver(() => {
injectCustomIcon();
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
if (document.body) {
init();
} else {
window.addEventListener("DOMContentLoaded", init, { once: true });
}
})();