Show a timer that shows the time left to post next message.
当前为
// ==UserScript==
// @name Trade Chat Timer on Button
// @namespace http://tampermonkey.net/
// @version 1.5
// @description Show a timer that shows the time left to post next message.
// @match https://www.torn.com/*
// ==/UserScript==
const STORAGE_KEY = "localStorage__Trade_Chat_Timer__Do_Not_Edit";
async function waitFor(selector, parent = document) {
return new Promise(resolve => {
const checkExist = () => {
const el = parent.querySelector(selector);
if (el) {
resolve(el);
} else {
requestAnimationFrame(checkExist);
}
};
checkExist();
});
}
(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"].time-left {
position: relative;
background-size: cover;
background-position: center;
}
#chatRoot [class*="minimized-menu-item__"][title="Trade"].time-complete {
position: relative;
background-size: cover;
background-position: center;
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="5" y="5" width="90" height="90" stroke="green" stroke-width="10" fill="none"/></svg>');
}
`;
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;
const updateTimerVisual = (timeLeft) => {
if (timeLeft > 0) {
tradeChatButton.classList.add("time-left");
tradeChatButton.classList.remove("time-complete");
tradeChatButton.style.backgroundImage = `url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="5" y="5" width="90" height="90" stroke="red" stroke-width="10" fill="none" stroke-dasharray="360" stroke-dashoffset="${360 * (1 - timeLeft / 60000)}"/></svg>')`;
} else {
tradeChatButton.classList.remove("time-left");
tradeChatButton.classList.add("time-complete");
tradeChatButton.style.backgroundImage = `url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="5" y="5" width="90" height="90" stroke="green" stroke-width="10" fill="none"/></svg>')`;
}
};
const setTimer = () => {
const timestamp = parseInt(localStorage.getItem(STORAGE_KEY) || Date.now());
const timeUntil = Math.max(60000 - (Date.now() - timestamp), 0);
updateTimerVisual(timeUntil);
if (!localStorage.getItem(STORAGE_KEY)) localStorage.setItem(STORAGE_KEY, Date.now());
};
const resetTimer = () => {
const timestamp = Date.now();
localStorage.setItem(STORAGE_KEY, timestamp);
updateTimerVisual(60000);
};
const throttle = (func, limit) => {
let lastFunc;
let lastRan;
return function(...args) {
if (!lastRan) {
func(...args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if ((Date.now() - lastRan) >= limit) {
func(...args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
};
async function checkForBlockMessage(chatBody) {
const lastMessage = chatBody.lastElementChild;
if (lastMessage && lastMessage.classList.contains("chat-box-body__block-message-wrapper___JjbKr") && lastMessage.textContent.includes("Trade chat allows one message per 60 seconds")) {
return true;
}
return false;
}
async function handleNewMessage(chat) {
const chatBody = chat.querySelector("[class*='chat-box-body___']");
const message = await new Promise(resolve => {
new MutationObserver((mutations, observer) => {
const mutation = mutations.find(mutation => mutation.addedNodes.length);
if (!mutation) return;
const node = mutation.addedNodes[0];
observer.disconnect();
resolve(node);
}).observe(chatBody, { childList: true });
});
if (await checkForBlockMessage(chatBody)) {
return;
}
resetTimer();
}
const attachKeyUpListener = (chat) => {
const textarea = chat.querySelector("textarea");
if (textarea) {
textarea.addEventListener("keyup", async e => {
if (e.key === "Enter") {
await handleNewMessage(chat);
}
});
}
};
const attachMutationObserver = (chat) => {
const chatBody = chat.querySelector("[class*='chat-box-body___']");
new MutationObserver(throttle(async (mutations) => {
if (await checkForBlockMessage(chatBody)) {
return;
}
}, 500)).observe(chatBody, { childList: true, subtree: true });
};
if (tradeChat) {
attachKeyUpListener(tradeChat);
attachMutationObserver(tradeChat);
}
tradeChatButton.addEventListener("click", async () => {
if (!tradeChatButton.className.includes("minimized-menu-item--open__")) {
tradeChat = await getTradeChat();
}
if (tradeChat) {
attachKeyUpListener(tradeChat);
attachMutationObserver(tradeChat);
}
});
document.addEventListener("click", async e => {
const specificButton = document.querySelector("button.chat-box-footer__send-icon-wrapper___fGx9E");
if (specificButton && specificButton.contains(e.target)) {
await handleNewMessage(tradeChat);
}
});
setTimer();
setInterval(setTimer, 100);
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__']");
}
})();