您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Show a timer that shows the time left to post next message, synchronized across tabs.
当前为
- // ==UserScript==
- // @name Trade Chat Timer on Button
- // @namespace http://tampermonkey.net/
- // @version 2.3
- // @description Show a timer that shows the time left to post next message, synchronized across tabs.
- // @match https://www.torn.com/*
- // ==/UserScript==
- const STORAGE_KEY = "localStorage__Trade_Chat_Timer__Do_Not_Edit";
- const timerChannel = new BroadcastChannel('tradeChatTimer');
- function waitFor(selector, parent = document) {
- return new Promise(resolve => {
- const observer = new MutationObserver(mutations => {
- mutations.forEach(() => {
- const el = parent.querySelector(selector);
- if (el) {
- observer.disconnect();
- resolve(el);
- }
- });
- });
- observer.observe(parent, { childList: true, subtree: true });
- });
- }
- (async () => {
- const addStyle = () => {
- if (!document.head.querySelector("#trade-chat-timer-style")) {
- const style = document.createElement('style');
- style.id = "trade-chat-timer-style";
- style.textContent = `
- #chatRoot [class*="minimized-menu-item__"][title="Trade"] {
- position: relative;
- }
- #chatRoot [class*="minimized-menu-item__"][title="Trade"] .timer-svg {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- pointer-events: none;
- }
- `;
- document.head.appendChild(style);
- }
- };
- addStyle();
- const tradeChatButton = await waitFor("#chatRoot [class*='minimized-menu-item__'][title='Trade']");
- let tradeChat = tradeChatButton?.className.includes("minimized-menu-item--open__") ? await getTradeChat() : null;
- let timerSvg = tradeChatButton?.querySelector('.timer-svg');
- if (tradeChatButton && !timerSvg) {
- timerSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- timerSvg.setAttribute("viewBox", "0 0 100 100");
- timerSvg.classList.add("timer-svg");
- timerSvg.innerHTML = '<rect x="5" y="5" width="90" height="90" stroke-width="10" fill="none" />';
- tradeChatButton.appendChild(timerSvg);
- }
- const timerRect = timerSvg?.querySelector('rect');
- let nextAllowedTime = new Date(localStorage.getItem(STORAGE_KEY) || Date.now());
- const updateTimerVisual = (timeLeft) => {
- const roundedTimeLeft = Math.round(timeLeft / 1000) * 1000; // Round to nearest second
- if (timerRect) {
- if (roundedTimeLeft > 0) {
- timerRect.setAttribute('stroke', 'red');
- timerRect.setAttribute('stroke-dasharray', '360');
- timerRect.setAttribute('stroke-dashoffset', 360 * (1 - roundedTimeLeft / 60000));
- } else {
- timerRect.setAttribute('stroke', 'green');
- timerRect.setAttribute('stroke-dasharray', 'none');
- timerRect.setAttribute('stroke-dashoffset', '0');
- }
- }
- };
- let timerInterval;
- const setTimer = () => {
- const now = new Date();
- const timeUntil = Math.max(nextAllowedTime - now, 0);
- updateTimerVisual(timeUntil);
- timerChannel.postMessage({ timeUntil });
- if (timeUntil === 0) {
- clearInterval(timerInterval);
- } else {
- requestAnimationFrame(setTimer);
- }
- };
- const resetTimer = () => {
- nextAllowedTime = new Date(Date.now() + 60000);
- localStorage.setItem(STORAGE_KEY, nextAllowedTime.toISOString());
- clearInterval(timerInterval);
- timerInterval = requestAnimationFrame(setTimer);
- timerChannel.postMessage({ reset: true, nextAllowedTime: nextAllowedTime.toISOString() });
- };
- timerChannel.onmessage = (event) => {
- if (event.data.timeUntil !== undefined) {
- updateTimerVisual(event.data.timeUntil);
- } else if (event.data.reset) {
- nextAllowedTime = new Date(event.data.nextAllowedTime);
- clearInterval(timerInterval);
- timerInterval = requestAnimationFrame(setTimer);
- }
- };
- async function checkForBlockMessage(chatBody) {
- const lastMessage = chatBody?.lastElementChild;
- return lastMessage && lastMessage.classList.contains("chat-box-body__block-message-wrapper___JjbKr") && lastMessage.textContent.includes("Trade chat allows one message per 60 seconds");
- }
- async function handleNewMessage(chat) {
- const chatBody = chat.querySelector("[class*='chat-box-body___']");
- return new Promise(resolve => {
- const observer = new MutationObserver(async (mutations) => {
- const mutation = mutations.find(mutation => mutation.addedNodes.length);
- if (!mutation) return;
- observer.disconnect();
- if (await checkForBlockMessage(chatBody)) {
- resolve(false);
- } else {
- resetTimer();
- resolve(true);
- }
- });
- observer.observe(chatBody, { childList: true });
- });
- }
- function attachChatListeners(chat) {
- const textarea = chat.querySelector("textarea");
- if (textarea) {
- textarea.addEventListener("keyup", async e => {
- if (e.key === "Enter") {
- await handleNewMessage(chat);
- }
- });
- }
- const sendButton = chat.querySelector("button.chat-box-footer__send-icon-wrapper___fGx9E");
- if (sendButton) {
- sendButton.addEventListener("click", async () => {
- await handleNewMessage(chat);
- });
- }
- }
- if (tradeChat) {
- attachChatListeners(tradeChat);
- }
- tradeChatButton?.addEventListener("click", async () => {
- if (!tradeChatButton.className.includes("minimized-menu-item--open__")) {
- tradeChat = await getTradeChat();
- }
- if (tradeChat) {
- attachChatListeners(tradeChat);
- }
- });
- setTimer(); // Initial timer setup
- timerInterval = requestAnimationFrame(setTimer);
- async function getTradeChat() {
- await waitFor("#chatRoot [class*='chat-box-header__']");
- return [...document.querySelectorAll("#chatRoot [class*='chat-box-header__']")].find(x => x.textContent === "Trade")?.closest("[class*='chat-box__']");
- }
- })();