Torn Chain Timer Overlay

Floating chain timer overlay for Torn — drag, resize, lock, keep it visible anywhere. Compact controls below.

// ==UserScript==
// @name         Torn Chain Timer Overlay
// @namespace    torn.chain.overlay
// @version      0.5
// @author       yoyo
// @description  Floating chain timer overlay for Torn — drag, resize, lock, keep it visible anywhere. Compact controls below.
// @license      MIT
// @match        https://www.torn.com/*
// @run-at       document-idle
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  // --- Selectors from your capture, plus fallbacks ---
  const SEL_TIMER_EXACT   = '#sidebar > div > div > div:nth-of-type(2) > div:nth-of-type(1) > a:nth-of-type(5) > div:nth-of-type(1) > span > p';
  const SEL_COUNTER_EXACT = '#sidebar > div > div > div:nth-of-type(2) > div:nth-of-type(1) > a:nth-of-type(5) > div:nth-of-type(1) > p:nth-of-type(2)';
  const q = (sel, root = document) => root.querySelector(sel);
  const findSidebar = () => document.getElementById('sidebar') || q('#sidebar, div[id*="sidebar"]');
  const classStartsWith = (el, prefix) => el && Array.from(el.classList || []).some(c => c.startsWith(prefix));

  const findTimer = () => {
    let el = q(SEL_TIMER_EXACT) || q('p.bar-timeleft___B9RGV');
    if (el) return el;
    const root = findSidebar() || document;
    return Array.from(root.querySelectorAll('p')).find(p =>
      classStartsWith(p, 'bar-timeleft') || /^\d{1,2}:\d{2}$/.test(p.textContent.trim())
    ) || null;
  };

  const findCounter = () => {
    let el = q(SEL_COUNTER_EXACT) || q('p.bar-value___uxnah');
    if (el) return el;
    const root = findSidebar() || document;
    return Array.from(root.querySelectorAll('p')).find(p =>
      classStartsWith(p, 'bar-value') && /\/\s*\d+k/i.test(p.textContent)
    ) || null;
  };

  // --- Styles (compact, controls underneath) ---
  GM_addStyle(`
    .cto-wrap {
      position: fixed; z-index: 2147483646;
      display: inline-flex; flex-direction: column; align-items: center; gap: 4px;
      background: rgba(18,18,18,.72);
      color: #fff; padding: 6px 8px 8px; border-radius: 10px;
      box-shadow: 0 8px 24px rgba(0,0,0,.35);
      -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
      user-select: none; cursor: grab;
    }
    .cto-wrap.cto-locked { background: rgba(18,18,18,.38); cursor: default; }
    .cto-wrap.cto-dragging { cursor: grabbing !important; }
    .cto-top { display: flex; flex-direction: column; align-items: center; gap: 2px; }
    .cto-text {
      font-weight: 900; letter-spacing: .5px; text-shadow: 0 1px 2px rgba(0,0,0,.35);
      line-height: 1; white-space: nowrap;
    }
    .cto-sub { font-weight: 600; opacity: .92; white-space: nowrap; }
    .cto-controls {
      display: flex; gap: 6px; margin-top: 2px;
      opacity: .85;
    }
    .cto-btn {
      font: 10px/1 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
      color: #e6e6e6; background: rgba(255,255,255,.12);
      border: 0; border-radius: 6px; padding: 3px 6px; cursor: pointer;
    }
    .cto-btn:hover { background: rgba(255,255,255,.18); }
    .cto-resize {
      position: absolute; right: 2px; bottom: 2px;
      width: 12px; height: 12px; border-radius: 3px;
      background:
        linear-gradient(135deg, rgba(255,255,255,.6) 0 50%, rgba(255,255,255,.25) 50% 100%);
      opacity: .7;
      cursor: nwse-resize;
    }
    .cto-wrap.cto-resizing { cursor: nwse-resize !important; }
    .cto-toast {
      position: fixed; left: 50%; top: 16px; transform: translateX(-50%);
      z-index: 2147483647; background: rgba(20,20,20,.95); color: #fff;
      padding: 8px 12px; border-radius: 8px; font: 13px/1.35 system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
      box-shadow: 0 6px 18px rgba(0,0,0,.35); pointer-events: none;
    }
  `);

  const showToast = (msg, ms = 1400) => {
    const el = document.createElement('div');
    el.className = 'cto-toast'; el.textContent = msg;
    document.body.appendChild(el); setTimeout(() => el.remove(), ms);
  };

  // --- State ---
  const stateKey = 'cto:v1';
  const loadState = () => { try { return JSON.parse(localStorage.getItem(stateKey) || '{}'); } catch { return {}; } };
  const saveState = (s) => localStorage.setItem(stateKey, JSON.stringify(s));

  const defaults = { x: 16, y: 80, fontPx: 34, locked: false, showChain: true };
  const st = Object.assign({}, defaults, loadState());

  // --- DOM ---
  const wrap = document.createElement('div');
  wrap.className = 'cto-wrap' + (st.locked ? ' cto-locked' : '');
  wrap.style.top = st.y + 'px';
  wrap.style.left = st.x + 'px';

  const top = document.createElement('div');
  top.className = 'cto-top';

  const txt = document.createElement('div');
  txt.className = 'cto-text';
  txt.style.fontSize = st.fontPx + 'px';
  txt.textContent = '--:--';

  const chain = document.createElement('div');
  chain.className = 'cto-sub';
  chain.textContent = '';
  chain.style.display = st.showChain ? '' : 'none';

  const controls = document.createElement('div');
  controls.className = 'cto-controls';

  const btnLock = document.createElement('button');
  btnLock.className = 'cto-btn';
  btnLock.title = 'Lock/Unlock overlay';
  btnLock.textContent = st.locked ? 'Unlock' : 'Lock';

  const btnToggleChain = document.createElement('button');
  btnToggleChain.className = 'cto-btn';
  btnToggleChain.title = 'Show/Hide chain counter';
  btnToggleChain.textContent = st.showChain ? 'Hide chain' : 'Show chain';

  const handle = document.createElement('div');
  handle.className = 'cto-resize';

  top.append(txt, chain);
  controls.append(btnLock, btnToggleChain);
  wrap.append(top, controls, handle);
  document.body.appendChild(wrap);

  // --- Dragging (Pointer Events, robust) ---
  let dragging = false;
  let dragOffset = { x: 0, y: 0 };
  const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));

  wrap.addEventListener('pointerdown', (e) => {
    if (st.locked) return;
    if (e.target === handle || e.target.classList.contains('cto-btn')) return; // allow handle/buttons to work
    // ALT+drag anywhere = resize (handled below); plain drag = move
    if (e.altKey) return;
    dragging = true;
    dragOffset.x = e.clientX - wrap.offsetLeft;
    dragOffset.y = e.clientY - wrap.offsetTop;
    wrap.classList.add('cto-dragging');
    wrap.setPointerCapture?.(e.pointerId);
    e.preventDefault();
  });

  const onPointerMove = (e) => {
    if (!dragging) return;
    const x = clamp(e.clientX - dragOffset.x, 0, window.innerWidth  - wrap.offsetWidth);
    const y = clamp(e.clientY - dragOffset.y, 0, window.innerHeight - wrap.offsetHeight);
    wrap.style.left = x + 'px';
    wrap.style.top  = y + 'px';
  };

  const endDrag = (e) => {
    if (!dragging) return;
    dragging = false;
    wrap.classList.remove('cto-dragging');
    wrap.releasePointerCapture?.(e.pointerId);
    st.x = parseInt(wrap.style.left, 10) || st.x;
    st.y = parseInt(wrap.style.top, 10) || st.y;
    saveState(st);
  };

  window.addEventListener('pointermove', onPointerMove, { passive: true });
  window.addEventListener('pointerup', endDrag);
  window.addEventListener('pointercancel', endDrag);
  document.addEventListener('visibilitychange', () => { if (document.hidden) endDrag({}); });

  // --- Resizing (Pointer Events) ---
  let resizing = null;
  const clampNum = (n, lo, hi) => Math.max(lo, Math.min(hi, n));

  handle.addEventListener('pointerdown', (e) => {
    if (st.locked) return;
    resizing = { id: e.pointerId, x0: e.clientX, y0: e.clientY, base: st.fontPx };
    wrap.classList.add('cto-resizing');
    handle.setPointerCapture?.(e.pointerId);
    e.preventDefault();
    e.stopPropagation();
  });

  const onResizeMove = (e) => {
    if (!resizing) return;
    const dx = e.clientX - resizing.x0;
    const dy = e.clientY - resizing.y0;
    const delta = Math.abs(Math.abs(dx) > Math.abs(dy) ? dx : dy);
    st.fontPx = clampNum(Math.round(resizing.base + delta * 0.25), 14, 160);
    txt.style.fontSize = st.fontPx + 'px';
  };

  const endResize = (e) => {
    if (!resizing) return;
    handle.releasePointerCapture?.(e.pointerId);
    resizing = null;
    wrap.classList.remove('cto-resizing');
    saveState(st);
  };

  window.addEventListener('pointermove', onResizeMove, { passive: true });
  window.addEventListener('pointerup', endResize);
  window.addEventListener('pointercancel', endResize);

  // Alt+drag anywhere on overlay = resize (nice fallback)
  let altResizing = null;
  wrap.addEventListener('pointerdown', (e) => {
    if (st.locked || !e.altKey) return;
    altResizing = { id: e.pointerId, x0: e.clientX, base: st.fontPx };
    wrap.classList.add('cto-resizing');
    wrap.setPointerCapture?.(e.pointerId);
    e.preventDefault();
  });
  window.addEventListener('pointermove', (e) => {
    if (!altResizing) return;
    const dx = e.clientX - altResizing.x0;
    st.fontPx = clampNum(Math.round(altResizing.base + dx * 0.25), 14, 160);
    txt.style.fontSize = st.fontPx + 'px';
  });
  const endAltResize = (e) => {
    if (!altResizing) return;
    wrap.releasePointerCapture?.(e.pointerId);
    altResizing = null;
    wrap.classList.remove('cto-resizing');
    saveState(st);
  };
  window.addEventListener('pointerup', endAltResize);
  window.addEventListener('pointercancel', endAltResize);

  // Ctrl+wheel resize
  wrap.addEventListener('wheel', (e) => {
    if (st.locked) return;
    if (!e.ctrlKey) return;
    e.preventDefault();
    const delta = -Math.sign(e.deltaY);
    st.fontPx = clampNum(st.fontPx + delta * 2, 14, 160);
    txt.style.fontSize = st.fontPx + 'px';
    saveState(st);
  }, { passive: false });

  // --- Buttons ---
  btnLock.addEventListener('click', () => {
    st.locked = !st.locked;
    wrap.classList.toggle('cto-locked', st.locked);
    btnLock.textContent = st.locked ? 'Unlock' : 'Lock';
    saveState(st);
    showToast(st.locked ? 'Overlay locked' : 'Overlay unlocked');
  });
  btnToggleChain.addEventListener('click', () => {
    st.showChain = !st.showChain;
    chain.style.display = st.showChain ? '' : 'none';
    btnToggleChain.textContent = st.showChain ? 'Hide chain' : 'Show chain';
    saveState(st);
  });

  // --- Live Updates from DOM ---
  const updateTexts = () => {
    const t = findTimer();
    if (t) txt.textContent = t.textContent.trim();
    const c = findCounter();
    if (c) chain.textContent = c.textContent.trim();
  };
  updateTexts();

  const mo = new MutationObserver(updateTexts);
  (findSidebar() ? [findSidebar()] : [document.body]).forEach(r =>
    r && mo.observe(r, { childList: true, subtree: true, characterData: true })
  );
  setInterval(updateTexts, 1000); // safety poll

  // Help
  document.addEventListener('keydown', (e) => {
    if (e.altKey && (e.key === 't' || e.key === 'T')) {
      showToast('Drag to move • Alt+Drag to resize • Ctrl+Wheel to resize • Buttons below • Lock to freeze');
    }
  });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址