add your friends local times to the clock in torn!
// ==UserScript==
// @name Time Zone Display
// @namespace http://tampermonkey.net/
// @version 1.8.2
// @description add your friends local times to the clock in torn!
// @author Pint-Shot-Riot
// @match https://www.torn.com/*
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const styleContent = `
#ups-tz-dropdown {
position: fixed; top: 55px; right: 10px;
background: rgba(30, 30, 30, 0.95); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 14px; padding: 12px; z-index: 999999;
box-shadow: 0 10px 30px rgba(0,0,0,0.6); min-width: 210px; display: none; color: #fff;
animation: upsSlideIn 0.2s ease-out;
pointer-events: auto !important;
}
@media screen and (max-width: 600px) {
#ups-tz-dropdown { min-width: 160px; padding: 8px; top: 50px; right: 5px; }
.ups-tz-item { padding: 6px 4px; }
.ups-tz-label { font-size: 10px; }
.ups-tz-time { font-size: 14px; }
.ups-tz-manage-btn { padding: 8px; margin-top: 6px; font-size: 10px; }
}
/* Torn-Style Nameplate Clock */
.ups-nameplate-time {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 10px;
padding: 0 8px;
height: 24px;
background: linear-gradient(180deg, #444 0%, #222 100%);
border: 1px solid #000;
border-radius: 4px;
color: #fff;
font-family: 'Orbitron', 'Roboto', monospace;
font-size: 13px;
font-weight: 700;
text-shadow: 0 1px 0 rgba(0,0,0,0.5);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
vertical-align: middle;
}
.ups-quick-add-wrap { display: inline-flex; align-items: center; gap: 5px; margin: 2px 5px; vertical-align: middle; }
.ups-quick-add {
display: inline-flex; align-items: center; justify-content: center;
background: linear-gradient(180deg, #444 0%, #222 100%);
color: #ccc; border-radius: 4px; padding: 4px 10px; height: 24px;
font-size: 10px; font-weight: bold; cursor: pointer;
border: 1px solid #000; text-shadow: 0 -1px 0 rgba(0,0,0,0.5);
transition: color 0.2s; text-transform: uppercase; font-family: Arial, sans-serif;
}
.ups-quick-add:hover { color: #fff; background: linear-gradient(180deg, #555 0%, #333 100%); }
@keyframes upsSlideIn { from { opacity: 0; transform: translateY(-10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
.ups-tz-header { display: flex; justify-content: flex-end; margin-bottom: -5px; }
.ups-tz-close { cursor: pointer; color: #666; font-size: 20px; font-weight: bold; padding: 0 5px; user-select: none; }
.ups-tz-close:hover { color: #fff; }
.ups-tz-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 6px; border-bottom: 1px solid rgba(255,255,255,0.05); }
.ups-tz-label { color: #aaa; font-size: 11px; text-transform: uppercase; font-weight: 600; }
.ups-tz-time { color: #fff; font-size: 16px; font-weight: 700; font-family: monospace; }
.ups-tz-manage-btn { background: rgba(255,255,255,0.15); color: #fff; text-align: center; padding: 12px; margin-top: 10px; border-radius: 10px; cursor: pointer; font-size: 11px; font-weight: bold; }
#tz-manager-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #111; z-index: 1000001; padding: 15px; box-sizing: border-box; overflow-y: auto; }
.ups-card { background: #1a1a1a; border-radius: 18px; padding: 20px; border: 1px solid #2a2a2a; margin-bottom: 20px; }
.ups-input { background: #000; color: #fff; border: 1px solid #333; padding: 15px; width: 100%; margin-bottom: 15px; border-radius: 10px; box-sizing: border-box; font-size: 16px; }
.ups-btn-save { width: 100%; padding: 16px; background: #6b8e23; color: #fff; border: none; border-radius: 10px; font-weight: 800; font-size: 14px; cursor: pointer; }
.ups-table { width: 100%; border-collapse: separate; border-spacing: 0 10px; }
.ups-table tr { background: #222; }
.ups-table td { padding: 15px; color: #fff; border-radius: 12px; }
.del-btn, .move-btn { cursor: pointer !important; border: 1px solid rgba(255,255,255,0.1); }
.del-btn { color: #ff4444; background: rgba(255, 68, 68, 0.1); padding: 8px 14px; border-radius: 8px; font-size: 11px; margin-left: 5px; }
.move-btn { color: #fff; background: rgba(255, 255, 255, 0.1); padding: 8px 12px; border-radius: 8px; font-size: 11px; margin-left: 4px; }
.ups-data-box { background: #000; color: #0f0; font-family: monospace; font-size: 12px; width: 100%; height: 60px; border: 1px solid #333; border-radius: 8px; padding: 10px; box-sizing: border-box; resize: none; margin-bottom: 10px; -webkit-user-select: text; user-select: text; }
.ups-data-btns { display: flex; gap: 10px; }
.ups-btn-small { flex: 1; padding: 10px; background: #444; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 11px; font-weight: bold; }
`;
function getZones() {
const raw = localStorage.getItem("upscript_tz_v2");
return raw ? JSON.parse(raw) : [{label: "Torn", timezone: "UTC"}];
}
function saveZones(zones) {
localStorage.setItem("upscript_tz_v2", JSON.stringify(zones));
renderList();
updateIOBox();
}
function updateIOBox() {
const io = document.getElementById("ups-io-box");
if (io) io.value = btoa(JSON.stringify(getZones()));
}
function isValidTimezone(tz) {
try { if (!tz) return false; Intl.DateTimeFormat(undefined, {timeZone: tz}); return true; }
catch (ex) { return false; }
}
function findTimezoneSmartly(input) {
if (!input) return null;
const clean = input.trim().toLowerCase().replace(/\s+/g, '_');
const offsetMatch = clean.match(/^(?:utc|gmt)?([+-]\d+)/);
if (offsetMatch) {
const hours = parseInt(offsetMatch[1]);
const inverseSign = hours > 0 ? "-" : "+";
return `Etc/GMT${inverseSign}${Math.abs(hours)}`;
}
const allZones = Intl.supportedValuesOf('timeZone');
let match = allZones.find(z => z.toLowerCase() === clean || z.toLowerCase().endsWith('/' + clean));
if (!match) { match = allZones.find(z => z.toLowerCase().includes(clean)); }
return match || null;
}
function formatZoneID(str) {
if (!str) return "";
if (str.startsWith('Etc/GMT')) return str;
if (!str.includes('/')) return str;
return str.split('/').map(part =>
part.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join('_')
).join('/');
}
function renderList() {
const list = document.getElementById("tz-list");
if (!list) return;
list.innerHTML = "";
getZones().forEach((z, i) => {
const row = list.insertRow();
row.innerHTML = `
<td><span style="font-size:14px; font-weight:700;">${z.label}</span></td>
<td style="text-align:right">
<button class="move-btn up-btn" data-index="${i}">▲</button>
<button class="move-btn down-btn" data-index="${i}">▼</button>
<button class="del-btn" data-index="${i}">REMOVE</button>
</td>`;
});
}
function openManager(prefillLabel = "", prefillZone = "") {
if (document.getElementById("tz-manager-overlay")) return;
const overlay = document.createElement("div");
overlay.id = "tz-manager-overlay";
overlay.innerHTML = `
<div style="max-width: 450px; margin: auto;">
<h2 style="color:#fff; text-align:center;">Time Zones</h2>
<div class="ups-card">
<input id="new-label" class="ups-input" placeholder="Name" value="${prefillLabel}">
<input id="new-zone" class="ups-input" placeholder="City or Offset" value="${prefillZone}">
<button id="add-tz" class="ups-btn-save">ADD TIMEZONE</button>
</div>
<table class="ups-table" id="tz-list"></table>
<div class="ups-card" style="margin-top:20px;">
<h3 style="color:#fff; font-size:12px; margin-bottom:10px; text-transform:uppercase;">Import / Export</h3>
<textarea id="ups-io-box" class="ups-data-box" readonly></textarea>
<div class="ups-data-btns">
<button id="ups-copy-btn" class="ups-btn-small">COPY CODE</button>
<button id="ups-import-btn" class="ups-btn-small">IMPORT CODE</button>
</div>
</div>
<button id="close-tz" style="width:100%; color:#666; background:none; border:none; padding:20px; cursor:pointer; font-weight:bold;">CLOSE</button>
</div>
`;
document.body.appendChild(overlay);
renderList();
updateIOBox();
overlay.addEventListener('click', (e) => {
if (e.target.id === 'close-tz') overlay.remove();
if (e.target.id === 'ups-copy-btn') {
const box = document.getElementById("ups-io-box");
box.select(); document.execCommand('copy');
e.target.textContent = "COPIED!"; setTimeout(() => e.target.textContent = "COPY CODE", 2000);
}
if (e.target.id === 'ups-import-btn') {
const code = prompt("Paste your export code here:");
if (code) {
try { const decoded = JSON.parse(atob(code)); if (Array.isArray(decoded)) saveZones(decoded); }
catch (err) { alert("Invalid code format."); }
}
}
if (e.target.id === 'add-tz') {
let l = document.getElementById("new-label").value.trim();
let zRaw = document.getElementById("new-zone").value.trim();
if (l && zRaw) {
let zFinal = formatZoneID(findTimezoneSmartly(zRaw) || zRaw);
if (isValidTimezone(zFinal)) {
const zones = getZones();
zones.push({label: l, timezone: zFinal});
saveZones(zones);
document.getElementById("new-label").value = "";
document.getElementById("new-zone").value = "";
injectProfileFeatures();
} else { alert("Invalid city or timezone."); }
}
}
const btn = e.target.closest('button');
if (btn && btn.dataset.index !== undefined) {
const index = parseInt(btn.dataset.index);
let zones = getZones();
if (btn.classList.contains('del-btn')) zones.splice(index, 1);
else if (btn.classList.contains('up-btn') && index > 0) [zones[index], zones[index - 1]] = [zones[index - 1], zones[index]];
else if (btn.classList.contains('down-btn') && index < zones.length - 1) [zones[index], zones[index + 1]] = [zones[index + 1], zones[index]];
saveZones(zones);
injectProfileFeatures();
}
});
}
function injectProfileFeatures() {
if (!window.location.href.includes('profiles.php')) return;
const nameElem = document.querySelector('.profile-wrapper .name') || document.querySelector('h1');
if (!nameElem) return;
const playerName = nameElem.textContent.trim().split('[')[0].trim();
const zones = getZones();
const savedZone = zones.find(z => z.label.toLowerCase() === playerName.toLowerCase());
// 1. NAMEPLATE DISPLAY
if (savedZone && !document.querySelector('.ups-nameplate-time')) {
const plateTime = document.createElement('span');
plateTime.className = 'ups-nameplate-time';
const updatePlate = () => {
plateTime.innerText = new Date().toLocaleTimeString("en-GB", { timeZone: savedZone.timezone, hour12: false, hour: '2-digit', minute: '2-digit' });
};
updatePlate();
setInterval(updatePlate, 15000);
nameElem.appendChild(plateTime);
}
// 2. QUICK ADD BUTTON
if (!document.querySelector('.ups-quick-add-wrap')) {
const target = document.querySelector('.actions-container') ||
document.querySelector('.profile-wrapper .actions') ||
document.querySelector('.basic-info');
if (target) {
let foundCity = "";
document.querySelectorAll('.profile-container .info-section ul li, .basic-info li').forEach(li => {
if (li.textContent.includes('Location:')) foundCity = li.textContent.replace('Location:', '').trim();
});
const wrap = document.createElement('div');
wrap.className = 'ups-quick-add-wrap';
const addBtn = document.createElement('button');
addBtn.className = 'ups-quick-add';
addBtn.innerText = savedZone ? 'EDIT TZ' : 'ADD TZ';
addBtn.onclick = (e) => { e.preventDefault(); openManager(playerName, foundCity); };
wrap.appendChild(addBtn);
if (target.classList.contains('actions') || target.classList.contains('actions-container')) target.appendChild(wrap);
else target.insertBefore(wrap, target.firstChild);
}
}
}
const style = document.createElement("style");
style.textContent = styleContent;
document.head.appendChild(style);
const dropdown = document.createElement("div");
dropdown.id = "ups-tz-dropdown";
document.body.appendChild(dropdown);
dropdown.addEventListener('click', (e) => {
if (e.target.id === 'ups-tz-close-btn') dropdown.style.display = 'none';
else if (e.target.id === 'ups-manage-trigger') { dropdown.style.display = 'none'; openManager(); }
});
document.addEventListener('mousedown', (e) => {
const clock = e.target.closest('#server-time') || e.target.closest('[class*="clock"]');
if (clock) {
if (dropdown.style.display === "block") dropdown.style.display = "none";
else { updateDropdownContent(); dropdown.style.display = "block"; }
} else if (!e.target.closest('#ups-tz-dropdown')) dropdown.style.display = "none";
});
function updateDropdownContent() {
let html = `<div class="ups-tz-header"><span class="ups-tz-close" id="ups-tz-close-btn">×</span></div>`;
getZones().forEach(z => {
try {
const time = new Date().toLocaleTimeString("en-GB", { timeZone: z.timezone, hour12: false, hour: '2-digit', minute: '2-digit' });
html += `<div class="ups-tz-item"><span class="ups-tz-label">${z.label}</span><span class="ups-tz-time">${time}</span></div>`;
} catch (e) { html += `<div class="ups-tz-item"><span class="ups-tz-label">${z.label}</span><span style="color:#ff4444">ERR</span></div>`; }
});
html += `<div class="ups-tz-manage-btn" id="ups-manage-trigger">SETTINGS</div>`;
dropdown.innerHTML = html;
}
const observer = new MutationObserver(injectProfileFeatures);
observer.observe(document.body, { childList: true, subtree: true });
injectProfileFeatures();
})();