Virus Status

Adds a custom icon to Torn's sidebar status icons

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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