您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Show a timer that shows the time left to post next message in trade chat with a sound notification
// ==UserScript== // @name Trade Chat Timer on Button for Chat 3.0 // @namespace http://tampermonkey.net/ // @version 3.2 // @description Show a timer that shows the time left to post next message in trade chat with a sound notification // @match https://www.torn.com/* // ==/UserScript== (() => { const STORAGE_KEY = 'tornTradeTimerEnd'; const TIMER_DURATION = 62000; let svgEl = null, rectEl = null, pathLength = 0; let observer = null; let animFrameId = 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 playNotificationSound = () => { try { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.setValueAtTime(800, audioContext.currentTime); oscillator.frequency.setValueAtTime(600, audioContext.currentTime + 0.1); gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.3); } catch (error) { console.log('Audio notification not supported:', error); } }; 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'; playNotificationSound(); localStorage.removeItem(STORAGE_KEY); }; const runTimer = async (btn) => { if (!btn) return; if (animFrameId) cancelAnimationFrame(animFrameId); btn.style.border = 'none'; let timerCompleted = false; 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 && !timerCompleted) { timerCompleted = true; cleanupTimer(btn); } else if (remaining > 0) { 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 waitForMessageSend = (textarea) => { return new Promise((resolve) => { const originalValue = textarea.value.trim(); if (!originalValue) { resolve(false); return; } const checkCleared = () => { if (!textarea.value.trim()) { resolve(true); } }; setTimeout(checkCleared, 100); const inputHandler = () => { if (!textarea.value.trim()) { textarea.removeEventListener('input', inputHandler); resolve(true); } }; textarea.addEventListener('input', inputHandler); setTimeout(() => { textarea.removeEventListener('input', inputHandler); resolve(false); }, 1000); }); }; const getTradeChat = () => { const chatContainers = document.querySelectorAll('[id*="public_trade"], [class*="root___"]:has([class*="title___"])'); for (const container of chatContainers) { const titleElement = container.querySelector('[class*="title___"]'); if (titleElement && titleElement.textContent.trim() === 'Trade') { return container; } } return document.getElementById('public_trade') || document.querySelector('[id*="public_trade"]'); }; const isTradeChat = (element) => { const tradeChat = getTradeChat(); if (!tradeChat) return false; return tradeChat.contains(element); }; const initializeTradeButton = (tradeBtn) => { if (!tradeBtn || tradeBtn._timerInitialized) 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'); tradeBtn._timerInitialized = true; }; const setupChatHandlers = (ta, send, tradeBtn) => { if (ta._timerSetup) return; const handleSendAttempt = async () => { if (isTradeChat(ta) && ta.value.trim()) { const messageSent = await waitForMessageSend(ta); if (messageSent) { startTimer(tradeBtn); } } }; ta.addEventListener('keydown', (e) => { if (e.key === 'Enter') { handleSendAttempt(); } }); if (send) { send.addEventListener('click', handleSendAttempt); } ta._timerSetup = true; }; const checkAndSetupElements = () => { const tradeBtn = document.getElementById('channel_panel_button:public_trade'); const tradeChat = getTradeChat(); if (tradeBtn) { initializeTradeButton(tradeBtn); } if (tradeChat && tradeBtn) { const ta = tradeChat.querySelector('textarea[class*="textarea___"]'); const send = tradeChat.querySelector('button[class*="iconWrapper___"]'); if (ta) { setupChatHandlers(ta, send, tradeBtn); } } }; const setupChatObserver = () => { if (observer) observer.disconnect(); const chatRoot = document.getElementById('chatRoot'); if (!chatRoot) { const docObserver = new MutationObserver(() => { if (document.getElementById('chatRoot')) { docObserver.disconnect(); setupChatObserver(); } }); docObserver.observe(document.body, { childList: true, subtree: true }); return; } let lastCheck = 0; observer = new MutationObserver(() => { const now = Date.now(); if (now - lastCheck > 100) { lastCheck = now; checkAndSetupElements(); } }); checkAndSetupElements(); observer.observe(chatRoot, { childList: true, subtree: true }); }; 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) { if (stored > Date.now()) { runTimer(tradeBtn); } else { cleanupTimer(tradeBtn); initializeTradeButton(tradeBtn); } } else { initializeTradeButton(tradeBtn); } } checkAndSetupElements(); } }); 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或关注我们的公众号极客氢云获取最新地址