Trade Chat Timer on Button

Show a timer that shows the time left to post next message

目前為 2025-04-09 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Trade Chat Timer on Button
// @namespace    http://tampermonkey.net/
// @version      2.9
// @description  Show a timer that shows the time left to post next message
// @match        https://www.torn.com/*
// ==/UserScript==
(() => {
    'use strict';
    const STORAGE_KEY = "localStorage__Trade_Chat_Timer__Do_Not_Edit";
    let timerId = null;
    let tradeChat = null;
    let tradeChatButton = null;
    let timerSvg = null;
    let timerRect = null;
    const chatRoot = document.querySelector('#chatRoot');
    let nextAllowedTime = new Date(localStorage.getItem(STORAGE_KEY) || Date.now().toString());
    function waitFor(selector, parent = document) {
        const el = parent.querySelector(selector);
        if (el)
            return Promise.resolve(el);
        return new Promise(resolve => {
            const observer = new MutationObserver(() => {
                const found = parent.querySelector(selector);
                if (found) {
                    observer.disconnect();
                    resolve(found);
                }
            });
            observer.observe(parent, {
                childList: true,
                subtree: true
            });
        });
    }
    async function init() {
        addStyle();
        tradeChatButton = (await waitFor("#channel_panel_button\\:public_trade", chatRoot));
        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);
            timerRect = timerSvg.querySelector('rect');
        }
        if (tradeChatButton && tradeChatButton.classList.contains("opened___Mwpgz")) {
            tradeChat = await getTradeChat();
            if (tradeChat)
                attachChatListeners(tradeChat);
        }
        tradeChatButton?.addEventListener("click", handleTradeChatButtonClick);
        startTimer();
        window.addEventListener('unload', cleanup);
        window.addEventListener('storage', onStorageChange);
    }
    function addStyle() {
        if (!document.head.querySelector("#trade-chat-timer-style")) {
            const style = document.createElement('style');
            style.id = "trade-chat-timer-style";
            style.textContent = `
                #chatRoot #channel_panel_button\\:public_trade {
                    position: relative;
                }
                #chatRoot #channel_panel_button\\:public_trade .timer-svg {
                    position: absolute;
                    top: 0;
                    left: 0;
                    width: 100%;
                    height: 100%;
                    pointer-events: none;
                }
            `;
            document.head.appendChild(style);
        }
    }
    async function getTradeChat() {
        return await waitFor("#public_trade", chatRoot);
    }
    function updateTimerVisual(timeLeft) {
        if (!timerRect)
            return;
        const roundedTimeLeft = Math.round(timeLeft / 1000) * 1000;
        if (timerRect.dataset.timeLeft === String(roundedTimeLeft))
            return;
        timerRect.dataset.timeLeft = String(roundedTimeLeft);
        let strokeColor;
        let dasharray;
        let dashoffset;
        if (roundedTimeLeft > 0) {
            strokeColor = 'red';
            dasharray = '360';
            dashoffset = String(360 * (1 - roundedTimeLeft / 60000));
        }
        else {
            strokeColor = 'green';
            dasharray = 'none';
            dashoffset = '0';
        }
        requestAnimationFrame(() => {
            timerRect?.setAttribute('stroke', strokeColor);
            timerRect?.setAttribute('stroke-dasharray', dasharray);
            timerRect?.setAttribute('stroke-dashoffset', dashoffset);
            timerRect?.getBoundingClientRect();
        });
    }
    function startTimer() {
        if (timerId !== null) {
            cancelAnimationFrame(timerId);
        }
        timerId = requestAnimationFrame(setTimer);
    }
    function setTimer() {
        const now = new Date();
        const timeUntil = Math.max(nextAllowedTime.getTime() - now.getTime(), 0);
        updateTimerVisual(timeUntil);
        if (timeUntil > 0) {
            timerId = requestAnimationFrame(setTimer);
        }
    }
    function resetTimer() {
        nextAllowedTime = new Date(Date.now() + 60000);
        localStorage.setItem(STORAGE_KEY, nextAllowedTime.toISOString());
        if (timerId !== null) {
            cancelAnimationFrame(timerId);
        }
        timerId = requestAnimationFrame(setTimer);
    }
    function onStorageChange(event) {
        if (event.key === STORAGE_KEY && event.newValue) {
            nextAllowedTime = new Date(event.newValue);
            startTimer();
        }
    }
    async function checkForBlockMessage(chatBody) {
        const lastMessage = chatBody.lastElementChild;
        const isBlockMessage = lastMessage &&
            lastMessage.classList.contains("root___NVIc9") &&
            lastMessage.textContent?.includes("Trade chat allows one message per 60 seconds");
        return Boolean(isBlockMessage);
    }
    function handleNewMessage(chat) {
        return new Promise(resolve => {
            const chatBody = chat.querySelector(".list___jqmw3");
            if (!chatBody) {
                resolve(true);
                return;
            }
            const observer = new MutationObserver(async (mutations) => {
                if (mutations.some(m => m.addedNodes.length)) {
                    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___V8HsV");
        const sendButton = chat.querySelector(".iconWrapper___tyRRU");
        if (textarea) {
            textarea.addEventListener("keyup", onKeyUp);
        }
        if (sendButton) {
            sendButton.addEventListener("click", onSendClick);
        }
        async function onKeyUp(e) {
            if (e.key === "Enter") {
                await handleNewMessage(chat);
            }
        }
        async function onSendClick() {
            await handleNewMessage(chat);
        }
    }
    async function handleTradeChatButtonClick() {
        if (!tradeChatButton?.classList.contains("opened___Mwpgz")) {
            tradeChat = await getTradeChat();
        }
        if (tradeChat) {
            attachChatListeners(tradeChat);
        }
    }
    function cleanup() {
        if (timerId !== null) {
            cancelAnimationFrame(timerId);
        }
        window.removeEventListener('storage', onStorageChange);
        tradeChatButton?.removeEventListener("click", handleTradeChatButtonClick);
    }
    init();
})();

QingJ © 2025

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