Time Zone Display

add your friends local times to the clock in torn!

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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">&times;</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();

})();