您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Show hospital time live with attack button on user search page.
// ==UserScript== // @name Torn Search Helper // @namespace torn-search-helper // @version 1.8 // @description Show hospital time live with attack button on user search page. // @author Rick-Grimes // @match https://www.torn.com/page.php?sid=UserList* // @match https://www.torn.com/loader.php?sid=UserList* // @license MIT // @grant GM_addStyle // @grant GM_registerMenuCommand // ==/UserScript== (function () { "use strict"; console.log("[TornSearchHelper] loaded"); // --- configuration & key storage --- const STORAGE_KEY = "tsh-apikey-v1"; let apiKey = localStorage.getItem(STORAGE_KEY) ?? ""; try { GM_registerMenuCommand("Torn Search Helper: Set API Key", () => { const v = prompt("Enter your PUBLIC Torn API key (16 chars) — leave empty to disable API usage:", apiKey || ""); if (v === null) return; if (v === "" || v.length === 16) { apiKey = v; localStorage.setItem(STORAGE_KEY, apiKey); alert("API key saved."); } else alert("API key must be 16 characters or blank."); }); } catch (e) { console.warn("[TornSearchHelper] GM_registerMenuCommand unavailable:", e); } // --- styles --- GM_addStyle(` .tsh-status { display:inline-block; margin-left:6px; font-size:12px; font-weight:600; color:#f39c12; white-space:nowrap; text-shadow:0 1px 0 rgba(0,0,0,0.6);} .tsh-status.urgent { color:#ff6b6b; } .tsh-status.small { font-weight:500; color:#bdbdbd; } .tsh-attack { display:inline-block; margin-left:6px; padding:2px 6px; font-size:11px; font-weight:700; color:#fff; background:#e74c3c; border-radius:3px; cursor:pointer; text-decoration:none; vertical-align:middle; line-height:1; box-shadow:0 1px 0 rgba(0,0,0,0.2);} .tsh-attack:hover { filter: brightness(0.95); } .tsh-attack:active { transform: translateY(1px); } .tsh-wrap { display:inline-flex; align-items:center; gap:6px; } `); // --- helpers --- function qAll(sel, root = document) { return Array.from(root.querySelectorAll(sel)); } function q(sel, root = document) { return root.querySelector(sel); } function pad(n){ return String(n).padStart(2,"0"); } function formatRemaining(seconds){ if(seconds<=0) return "00:00:00"; const h=Math.floor(seconds/3600), m=Math.floor((seconds%3600)/60), s=seconds%60; return `${pad(h)}:${pad(m)}:${pad(s)}`; } const lastUpdated = new Map(); const UPDATE_INTERVAL_MS = 10_000; function collectRows(){ let listItems = qAll("ul.user-info-list-wrap li[class^='user'], ul.user-info-list-wrap li[class*='user']"); if(listItems.length===0){ return qAll("ul.user-info-list-wrap li").filter(li=>li.querySelector("a[href*='profiles.php?XID=']")); } return listItems; } function extractUserFromLi(li){ const a = li.querySelector("a[href*='profiles.php?XID=']"); if(!a) return null; const id = a.href.match(/XID=(\d+)/)?.[1]; return id ? {id, nameEl:a, li, expander: li.querySelector("div.expander")||li.querySelector(".expander")} : null; } function ensureNodes(user){ const li = user.li; let iconsWrap = q(".icons-wrap", li) || q("ul.big.svg", li); if(!iconsWrap) iconsWrap = user.expander; if(!iconsWrap) return null; let wrapper = iconsWrap.nextElementSibling; if(!wrapper || !wrapper.classList?.contains("tsh-wrap")){ wrapper = document.createElement("span"); wrapper.className="tsh-wrap"; iconsWrap.parentNode.insertBefore(wrapper, iconsWrap.nextSibling); } let timerNode = wrapper.querySelector(".tsh-status"); if(!timerNode){ timerNode=document.createElement("span"); timerNode.className="tsh-status"; wrapper.appendChild(timerNode); } let attackNode = wrapper.querySelector(".tsh-attack"); if(!attackNode){ attackNode=document.createElement("a"); attackNode.className="tsh-attack"; attackNode.textContent="Attack"; attackNode.href="#"; attackNode.setAttribute("role","button"); attackNode.addEventListener("click",(ev)=>{ ev.preventDefault(); if(attackNode.dataset.attackHref) window.open(attackNode.dataset.attackHref,"_blank"); },{passive:false}); wrapper.appendChild(attackNode); } return {wrapper, timerNode, attackNode}; } function parseFallback(li){ const iconLis=qAll(".icons-wrap li, ul.big.svg li", li); for(const icon of iconLis){ const title = icon.getAttribute("title")||""; if(/Hospital|Jail|Traveling|Abroad/i.test(title)){ const timeMatch = title.match(/data-time=['"]?(\d+)/i); if(timeMatch) return {state:"Hospital", untilInSeconds:parseInt(timeMatch[1],10)}; const timerSpan=icon.querySelector(".timer"); if(timerSpan){ const dt = timerSpan.getAttribute("data-time"); if(dt && /^\d+$/.test(dt)) return {state:"Hospital", untilInSeconds:parseInt(dt,10)}; return {state:"Hospital", text:timerSpan.textContent.trim()||title}; } return {state:title.replace(/<[^>]+>/g,"").replace(/\s+/g," ").trim()}; } } return null; } async function fetchStatus(id){ if(!apiKey) return null; try{ const resp = await fetch(`https://api.torn.com/user/${id}?selections=basic&key=${apiKey}`); const json = await resp.json(); if(!json || json.error) return null; return json.status||null; }catch(e){ console.error(e); return null; } } async function updateOne(user){ const now = Date.now(); if(now - (lastUpdated.get(user.id)||0)<UPDATE_INTERVAL_MS) return; lastUpdated.set(user.id, now); const nodes = ensureNodes(user); if(!nodes) return; const {timerNode, attackNode} = nodes; timerNode.classList.remove("urgent","small"); timerNode.textContent=""; attackNode.dataset.attackHref=`https://www.torn.com/loader.php?sid=attack&user2ID=${user.id}`; let status = apiKey ? await fetchStatus(user.id) : null; let st = null; if(!status){ const fallback=parseFallback(user.li); if(fallback){ if(fallback.untilInSeconds!==undefined){ const untilUnix=Math.floor(Date.now()/1000)+fallback.untilInSeconds; timerNode.dataset.until=untilUnix; timerNode.textContent=formatRemaining(fallback.untilInSeconds); if(fallback.untilInSeconds<300) timerNode.classList.add("urgent"); st=fallback.state; } else if(fallback.text){ timerNode.textContent=fallback.text; timerNode.classList.add("small"); st=fallback.state; } else{ timerNode.textContent=String(fallback.state).slice(0,30); timerNode.classList.add("small"); st=fallback.state; } } else timerNode.textContent=""; } else{ st=status.state||"Unknown"; if(st==="Hospital"||st==="Jail"){ const nowSec=Math.floor(Date.now()/1000); const until=parseInt(status.until||0,10); const remaining=Math.max(0,until-nowSec); timerNode.dataset.until=until; timerNode.textContent=formatRemaining(remaining); if(remaining<300) timerNode.classList.add("urgent"); } else if(st==="Traveling"||st==="Abroad"){ let desc=status.description||st; let out=desc; const m=desc.match(/Traveling to (.+)/i); if(m) out=`► ${m[1].slice(0,30)}`; const m2=desc.match(/In (.+)/i); if(m2) out=`${m2[1].slice(0,30)}`; timerNode.textContent=out; timerNode.classList.add("small"); } else{ timerNode.textContent=st; timerNode.classList.add("small"); } } // Show attack button only if not in Jail/Federal Jail/Traveling/Abroad attackNode.style.display = /Jail|Traveling|Abroad/i.test(st) ? "none" : "inline-block"; } async function updateVisibleSet(){ const rows=collectRows(); if(!rows || !rows.length) return; const users=[]; rows.forEach(li=>{ const u=extractUserFromLi(li); if(u) users.push(u); }); users.forEach(u=>ensureNodes(u)); for(const u of users){ updateOne(u); await new Promise(r=>setTimeout(r,120)); } } function tickTimers(){ const nodes=qAll(".tsh-status"); const nowSec=Math.floor(Date.now()/1000); nodes.forEach(node=>{ if(!node.dataset.until) return; const until=parseInt(node.dataset.until,10); const remaining=Math.max(0,until-nowSec); node.textContent=formatRemaining(remaining); node.classList.toggle("urgent",remaining<300); }); } setTimeout(()=>updateVisibleSet(),1200); const listWrap=document.querySelector("ul.user-info-list-wrap"); const observerTarget=listWrap||document.body; const mo=new MutationObserver(muts=>{ if(muts.some(m=>m.addedNodes.length>0)){ if(window.__tsh_update_timer) clearTimeout(window.__tsh_update_timer); window.__tsh_update_timer=setTimeout(()=>updateVisibleSet(),300); } }); mo.observe(observerTarget,{childList:true,subtree:true}); setInterval(()=>{ tickTimers(); updateVisibleSet(); },1000); console.log("[TornSearchHelper] initialized (accurate server-synced timers + Attack button logic)"); })();