您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Show a timer that shows the time left to post next message
// ==UserScript== // @name Trade Chat Timer on Button for Chat 3.0 // @namespace http://tampermonkey.net/ // @version 3.0 // @description Show a timer that shows the time left to post next message // @match https://www.torn.com/* // ==/UserScript== (() => { const STORAGE_KEY = 'tornTradeTimerEnd'; const TIMER_DURATION = 62000; let animFrameId = null; let svgEl = null, rectEl = null, pathLength = 0; let observer = null; const createSVGElement = (type, attributes = {}) => { const el = document.createElementNS('http://www.w3.org/2000/svg', type); Object.entries(attributes).forEach(([key, value]) => el.setAttribute(key, value)); return el; }; const setupOverlay = (btn) => { const { width, height } = btn.getBoundingClientRect(); if (!svgEl) { svgEl = createSVGElement('svg', { id: 'trade-timer-overlay', style: 'position:absolute;top:0;left:0;z-index:1000;pointer-events:none' }); rectEl = createSVGElement('rect', { x: '1.5', y: '1.5', fill: 'none', 'stroke-width': '3' }); svgEl.appendChild(rectEl); btn.appendChild(svgEl); } Object.entries({ width, height }).forEach(([key, value]) => svgEl.setAttribute(key, value)); Object.entries({ width: width - 3, height: height - 3 }).forEach(([key, value]) => rectEl.setAttribute(key, value)); pathLength = 2 * (width + height - 6); rectEl.setAttribute('stroke-dasharray', pathLength); }; const updateTimerVisual = (remainingMs) => { if (!rectEl) return; const isComplete = remainingMs <= 0; rectEl.setAttribute('stroke', isComplete ? 'green' : 'red'); rectEl.setAttribute('stroke-dasharray', isComplete ? 'none' : pathLength); if (!isComplete) { const offset = pathLength * (1 - remainingMs / TIMER_DURATION); rectEl.setAttribute('stroke-dashoffset', offset); } }; const cleanupTimer = (btn) => { if (animFrameId) { cancelAnimationFrame(animFrameId); animFrameId = null; } if (svgEl) { svgEl.remove(); svgEl = null; rectEl = null; } if (btn) btn.style.border = '3px solid green'; localStorage.removeItem(STORAGE_KEY); }; const runTimer = (btn) => { if (!btn) return; if (animFrameId) cancelAnimationFrame(animFrameId); btn.style.border = 'none'; // Force reinitialize SVG elements if (svgEl) { svgEl.remove(); svgEl = null; rectEl = null; } setupOverlay(btn); const animate = () => { const endTime = parseInt(localStorage.getItem(STORAGE_KEY)); const remaining = endTime - Date.now(); if (remaining <= 0) { cleanupTimer(btn); } else { updateTimerVisual(remaining); animFrameId = requestAnimationFrame(animate); } }; const endTime = parseInt(localStorage.getItem(STORAGE_KEY)); updateTimerVisual(endTime - Date.now()); animFrameId = requestAnimationFrame(animate); }; const startTimer = (btn) => { if (!btn) return; localStorage.setItem(STORAGE_KEY, Date.now() + TIMER_DURATION); runTimer(btn); }; const handleSend = (ta, btn) => { if (!ta || !btn) return; const checkAndStart = () => !ta.value.trim() && startTimer(btn); checkAndStart() || setTimeout(checkAndStart, 200); }; const initializeTradeButton = (tradeBtn) => { if (!tradeBtn) return; if (getComputedStyle(tradeBtn).position === 'static') { tradeBtn.style.position = 'relative'; } const stored = parseInt(localStorage.getItem(STORAGE_KEY)); stored && stored > Date.now() ? runTimer(tradeBtn) : (tradeBtn.style.border = '3px solid green'); }; const setupChat = () => { const tradeBtn = document.getElementById('channel_panel_button:public_trade'); const ta = document.querySelector('textarea.textarea___V8HsV'); const send = document.querySelector('button.iconWrapper___tyRRU'); if (tradeBtn) initializeTradeButton(tradeBtn); if (tradeBtn && ta && send) { const handlers = { keydown: (e) => e.key === 'Enter' && (e.preventDefault(), handleSend(ta, tradeBtn)), input: () => !ta.value.trim() && handleSend(ta, tradeBtn), click: () => handleSend(ta, tradeBtn) }; Object.entries(handlers).forEach(([event, handler]) => { const prop = `_timer${event}Handler`; if (ta[prop]) ta.removeEventListener(event, ta[prop]); if (send[prop]) send.removeEventListener(event, send[prop]); const element = event === 'click' ? send : ta; element[prop] = handler; element.addEventListener(event, handler); }); } }; const setupChatObserver = () => { if (observer) observer.disconnect(); const chatRoot = document.getElementById('chatRoot'); if (!chatRoot) { const docObserver = new MutationObserver(() => { const chatRoot = document.getElementById('chatRoot'); if (chatRoot) { docObserver.disconnect(); setupChatObserver(); } }); docObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['id', 'class', 'style'] }); return; } observer = new MutationObserver((mutations) => { const hasRelevantChanges = mutations.some(mutation => mutation.type === 'childList' || (mutation.type === 'attributes' && ['class', 'style'].includes(mutation.attributeName)) ); if (hasRelevantChanges) setTimeout(setupChat, 100); }); setupChat(); observer.observe(chatRoot, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style'] }); }; document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { const tradeBtn = document.getElementById('channel_panel_button:public_trade'); if (tradeBtn) { const stored = parseInt(localStorage.getItem(STORAGE_KEY)); if (stored && stored > Date.now()) { runTimer(tradeBtn); } else { initializeTradeButton(tradeBtn); } setupChat(); } } }); window.addEventListener('storage', (e) => { if (e.key === STORAGE_KEY) { const tradeBtn = document.getElementById('channel_panel_button:public_trade'); if (!tradeBtn) return; const val = parseInt(e.newValue); val && val > Date.now() ? runTimer(tradeBtn) : cleanupTimer(tradeBtn); } }); document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', setupChatObserver) : setupChatObserver(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址