TORN Live Clock (No iframe)

Live on-page clock overlay for TORN with timezone, 12/24h, font-size, draggable, persistent

当前为 2025-09-08 提交的版本,查看 最新版本

// ==UserScript==
// @name         TORN Live Clock (No iframe)
// @namespace    tm.torn.clock.overlay
// @version      1.1
// @description  Live on-page clock overlay for TORN with timezone, 12/24h, font-size, draggable, persistent
// @author       TrippingMartian
// @license Attribution Required
// Free to use, modify, and redistribute,
// but please credit "TrippingMartian" as the original author.
// @match        https://www.torn.com/*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const LS = 'tm_live_clock_v1';
  const defaults = {
    tz: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
    is24: true,
    font: 16,       // px
    x: null, y: null,
    visible: true
  };

  const load = () => {
    try { return Object.assign({}, defaults, JSON.parse(localStorage.getItem(LS) || '{}')); }
    catch { return { ...defaults }; }
  };
  const save = (cfg) => localStorage.setItem(LS, JSON.stringify(cfg));

  const cfg = load();

  // Container
  const box = document.createElement('div');
  box.style.cssText = `
    position: fixed; z-index: 999999; right: 16px; bottom: 16px;
    background: rgba(18,18,18,.85); color: #e8e8e8;
    border: 1px solid rgba(255,255,255,.15);
    border-radius: 6px; box-shadow: 0 6px 16px rgba(0,0,0,.35);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
    user-select: none; display: ${cfg.visible ? 'block' : 'none'};
  `;

  if (Number.isFinite(cfg.x) && Number.isFinite(cfg.y)) {
    box.style.left = cfg.x + 'px';
    box.style.top = cfg.y + 'px';
    box.style.right = 'auto';
    box.style.bottom = 'auto';
  }

  // Header (drag handle)
  const bar = document.createElement('div');
  bar.textContent = 'Clock';
  bar.style.cssText = `
    height: 20px; line-height: 20px; padding: 0 6px; font-size: 12px;
    background: rgba(255,255,255,.06); color:#bbb; cursor: move; display:flex; gap:6px;
  `;

  const btnSet = document.createElement('button');
  btnSet.textContent = 'Set';
  const btnHide = document.createElement('button');
  btnHide.textContent = 'Hide';
  [btnSet, btnHide].forEach(b=>{
    b.style.cssText = `margin-left:auto;border:0;background:rgba(255,255,255,.10);
                       color:#ddd;border-radius:4px;padding:0 6px;height:16px;cursor:pointer;`;
    b.onmouseenter = () => b.style.background = 'rgba(255,255,255,.18)';
    b.onmouseleave = () => b.style.background = 'rgba(255,255,255,.10)';
  });
  bar.appendChild(btnSet);
  bar.appendChild(btnHide);

  // Content
  const body = document.createElement('div');
  body.style.cssText = `padding: 6px 10px;`;
  const timeEl = document.createElement('div');
  timeEl.style.cssText = `font-weight:600; font-size:${cfg.font}px; letter-spacing:0.5px;`;
  const tzEl = document.createElement('div');
  tzEl.style.cssText = `font-size: 11px; color:#9aa; margin-top:2px;`;

  // Signature (hidden by default, shows on hover)
  const sig = document.createElement('div');
  sig.textContent = 'Made by TrippingMartian';
  sig.style.cssText = `
    font-size: 10px; color:#666; margin-top:4px;
    text-align:right; font-style:italic;
    display:none;
  `;

  body.appendChild(timeEl);
  body.appendChild(tzEl);
  body.appendChild(sig);

  box.appendChild(bar);
  box.appendChild(body);
  document.body.appendChild(box);

  // Hover behavior for signature
  box.addEventListener('mouseenter', () => sig.style.display = 'block');
  box.addEventListener('mouseleave', () => sig.style.display = 'none');

  // Update loop
  function render() {
    try {
      const now = new Date();
      const opts = { hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone: cfg.tz, hour12: !cfg.is24 };
      timeEl.textContent = new Intl.DateTimeFormat(undefined, opts).format(now);
      tzEl.textContent = `${cfg.tz} • ${cfg.is24 ? '24-hour' : '12-hour'}`;
    } catch {
      timeEl.textContent = 'Invalid timezone';
      tzEl.textContent = '';
    }
  }
  render();
  setInterval(render, 1000);

  // Dragging
  (function enableDrag() {
    let sx=0, sy=0, ox=0, oy=0, dragging=false;
    bar.addEventListener('mousedown', (e)=>{
      dragging = true; sx = e.clientX; sy = e.clientY;
      const r = box.getBoundingClientRect(); ox = r.left; oy = r.top;
      e.preventDefault();
    });
    window.addEventListener('mousemove', (e)=>{
      if (!dragging) return;
      const nx = ox + (e.clientX - sx), ny = oy + (e.clientY - sy);
      box.style.left = nx + 'px'; box.style.top = ny + 'px';
      box.style.right = 'auto'; box.style.bottom = 'auto';
    });
    window.addEventListener('mouseup', ()=>{
      if (!dragging) return; dragging = false;
      const r = box.getBoundingClientRect();
      cfg.x = Math.round(r.left); cfg.y = Math.round(r.top); save(cfg);
    });
  })();

  // Controls
  function setClock() {
    const tz = prompt(
      'Enter IANA timezone (e.g. Asia/Singapore, Europe/London, America/New_York):',
      cfg.tz
    );
    if (tz === null) return;

    const fmt = prompt('Use 24-hour format? (yes/no)', cfg.is24 ? 'yes' : 'no');
    if (fmt === null) return;

    const font = prompt('Font size in px (e.g. 16, 18, 20):', String(cfg.font));
    if (font === null) return;

    cfg.tz = tz.trim() || cfg.tz;
    cfg.is24 = /^y/i.test(fmt.trim());
    cfg.font = Math.max(10, parseInt(font, 10) || cfg.font);
    timeEl.style.fontSize = cfg.font + 'px';
    save(cfg);
    render();
  }

  btnSet.addEventListener('click', setClock);
  btnHide.addEventListener('click', ()=>{
    cfg.visible = false; save(cfg); box.style.display = 'none';
  });

  // Hotkeys
  window.addEventListener('keydown', (e)=>{
    if (e.ctrlKey && e.shiftKey && e.code === 'KeyC') setClock();       // Ctrl+Shift+C
    if (e.ctrlKey && e.shiftKey && e.code === 'KeyX') {                 // Ctrl+Shift+X
      cfg.visible = !cfg.visible; save(cfg);
      box.style.display = cfg.visible ? 'block' : 'none';
    }
  });
})();