您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Matches forum threads with API data, displays their timestamps, and allows for custom highlighting of threads based on the number of days back a user wants highlighted, while using local storage to save API results.
// ==UserScript== // @name Torn Forum Timestamps Highlighter with Local Storage // @namespace http://tampermonkey.net/ // @version 2.75 // @description Matches forum threads with API data, displays their timestamps, and allows for custom highlighting of threads based on the number of days back a user wants highlighted, while using local storage to save API results. // @author ShadowBirb // @license MIT // @match https://www.torn.com/forums.php* // @grant GM_xmlhttpRequest // @connect api.torn.com // ==/UserScript== (function () { 'use strict'; // Function to get API key from local storage const getApiKey = () => localStorage.getItem('tornApiKey'); // Function to set API key to local storage const setApiKey = (key) => localStorage.setItem('tornApiKey', key); // Function to save thread data to local storage const saveThreadToLocalStorage = (threadId, data) => { const threads = JSON.parse(localStorage.getItem('threadData')) || {}; threads[threadId] = { data, timestamp: Date.now() }; localStorage.setItem('threadData', JSON.stringify(threads)); }; // Function to get thread data from local storage const getThreadFromLocalStorage = (threadId) => { const threads = JSON.parse(localStorage.getItem('threadData')) || {}; return threads[threadId] || null; }; // Function to add buttons to the page const addButtons = () => { const forumsHeader = document.querySelector('#skip-to-content'); if (!forumsHeader || document.querySelector('#custom-buttons-container')) return; const buttonContainer = document.createElement('div'); buttonContainer.id = 'custom-buttons-container'; buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '10px'; buttonContainer.style.marginTop = '10px'; const runButton = document.createElement('button'); runButton.textContent = 'Run Forum Timestamps'; runButton.style.backgroundColor = '#4CAF50'; runButton.style.color = 'white'; runButton.style.border = 'none'; runButton.style.borderRadius = '5px'; runButton.style.padding = '5px 15px'; runButton.style.cursor = 'pointer'; runButton.addEventListener('click', () => { processThreads(); applyCustomStyles(); }); const apiKeyButton = document.createElement('button'); apiKeyButton.textContent = 'Public API Key'; apiKeyButton.style.backgroundColor = 'gray'; apiKeyButton.style.color = 'white'; apiKeyButton.style.border = 'none'; apiKeyButton.style.borderRadius = '5px'; apiKeyButton.style.padding = '5px 15px'; apiKeyButton.style.cursor = 'pointer'; apiKeyButton.addEventListener('click', showApiKeyPrompt); buttonContainer.appendChild(runButton); buttonContainer.appendChild(highlightButton); buttonContainer.appendChild(apiKeyButton); forumsHeader.after(buttonContainer); forumsHeader.remove(); }; // Button to highlight threads const highlightButton = document.createElement('button'); highlightButton.textContent = 'Highlight Threads'; highlightButton.style.backgroundColor = 'rgb(25,115,225)'; highlightButton.style.color = 'white'; highlightButton.style.border = 'none'; highlightButton.style.borderRadius = '5px'; highlightButton.style.padding = '5px 15px'; highlightButton.style.cursor = 'pointer'; highlightButton.addEventListener('click', () => showToast()); const showToast = () => { const toast = document.createElement('div'); toast.style.position = 'fixed'; toast.style.bottom = '70px'; toast.style.right = '20px'; toast.style.padding = '15px'; toast.style.backgroundColor = 'rgb(51,51,51)'; toast.style.color = 'white'; toast.style.borderRadius = '5px'; toast.style.boxShadow = '0 2px 10px rgba(0,0,0,0.5)'; toast.style.zIndex = '1000'; toast.textContent = 'Enter how many days ago to highlight threads:'; const input = document.createElement('input'); input.type = 'number'; input.style.marginLeft = '10px'; input.style.border = '1px solid white'; input.style.borderRadius = '3px'; input.style.padding = '5px'; toast.appendChild(input); const button = document.createElement('button'); button.textContent = 'Highlight'; button.style.marginLeft = '10px'; button.style.backgroundColor = '#007BFF'; button.style.color = 'white'; button.style.border = 'none'; button.style.padding = '5px 10px'; button.style.borderRadius = '3px'; button.style.cursor = 'pointer'; button.addEventListener('click', () => { const days = parseInt(input.value, 10); if (isNaN(days) || days <= 0) { alert('Please enter a valid number of days.'); return; } highlightThreads(days); document.body.removeChild(toast); }); toast.appendChild(button); document.body.appendChild(toast); }; // Add the highlight button to the button container document.querySelector('#custom-buttons-container')?.appendChild(highlightButton); // Highlight threads based on cached data const highlightThreads = (daysAgo) => { const cutoffTime = Date.now() - daysAgo * 24 * 60 * 60 * 1000; document.querySelectorAll('.thread-name').forEach((threadElement) => { const href = threadElement.getAttribute('href'); if (!href) return; const match = href.match(/t=(\d+)/); if (match) { const threadId = match[1]; const threads = JSON.parse(localStorage.getItem('threadData')) || {}; const cachedThread = threads[threadId]; if (cachedThread && cachedThread.data.thread.first_post_time * 1000 > cutoffTime) { // Highlight thread threadElement.style.backgroundColor = 'rgba(185,180,200,.5)'; // Gray background } } }); }; // Apply width to specific thread name elements const applyCustomStyles = () => { document.querySelectorAll('.d .forums-committee-wrap .forums-committee .threads-list > li .thread-name-wrap .thread-name').forEach((element) => { element.style.width = '480px'; // Format thread name elements document.querySelectorAll('li.thread-name[role="heading"][aria-level="5"]').forEach((element) => { element.style.width = '515px'; // Format thread name elements and apply max-width: max-content document.querySelectorAll('.title-flex-parent').forEach((element) => { element.style.maxWidth = 'max-content'; // Remove the headers document.querySelectorAll('.thread-pages.t-hide.m-hide, .last-post.t-overflow, .last-post.title-divider.divider-spiky').forEach(element => { element.remove(); }); }); }); }); }; // Start by adding buttons after 2.5 seconds setTimeout(() => addButtons(), 2500); const formatTimestamp = (timestamp) => { const date = new Date(timestamp * 1000); return date.toLocaleString(); }; const processThreads = () => { console.log('Fetching API data for threads...'); const threadLinks = Array.from(document.querySelectorAll('.thread-name')) .map(link => { const href = link.getAttribute('href'); if (!href) return null; const match = href.match(/t=(\d+)/); return match ? match[1] : null; }) .filter(id => id !== null); if (threadLinks.length === 0) { console.error('No thread IDs found on the page.'); return; } const apiKey = getApiKey(); threadLinks.forEach(threadId => { const cachedThread = getThreadFromLocalStorage(threadId); if (cachedThread && Date.now() - cachedThread.timestamp < 7 * 24 * 60 * 60 * 1000) { // Use cache if less than 7 days old appendTimestampToThread(threadId, cachedThread.data); return; } const apiUrl = `https://api.torn.com/v2/forum/${threadId}/thread?key=${apiKey}`; GM_xmlhttpRequest({ method: 'GET', url: apiUrl, onload: function (response) { if (response.status === 200) { const data = JSON.parse(response.responseText); if (data && data.thread) { saveThreadToLocalStorage(threadId, data); appendTimestampToThread(threadId, data); } } } }); }); }; const appendTimestampToThread = (threadId, data) => { document.querySelectorAll('.thread-name').forEach((threadElement) => { const href = threadElement.getAttribute('href'); if (!href) return; const match = href.match(/t=(\d+)/); if (match && match[1] === threadId) { const timestamp = formatTimestamp(data.thread.first_post_time); const timestampElement = document.createElement('span'); timestampElement.style.color = 'gray'; timestampElement.style.marginLeft = '10px'; timestampElement.textContent = `Posted: ${timestamp}`; threadElement.appendChild(timestampElement); } }); }; const showApiKeyPrompt = () => { const toast = document.createElement('div'); toast.style.position = 'fixed'; toast.style.bottom = '70px'; toast.style.right = '20px'; toast.style.padding = '15px'; toast.style.backgroundColor = 'rgb(51,51,51)'; toast.style.color = 'white'; toast.style.borderRadius = '5px'; toast.style.boxShadow = '0 2px 10px rgba(0,0,0,0.5)'; toast.style.zIndex = '1000'; toast.textContent = 'Enter your public API key:'; const input = document.createElement('input'); input.type = 'text'; input.style.marginLeft = '10px'; input.style.border = '1px solid white'; input.style.borderRadius = '3px'; input.style.padding = '5px'; toast.appendChild(input); const button = document.createElement('button'); button.textContent = 'Save Key'; button.style.marginLeft = '10px'; button.style.backgroundColor = 'gray'; button.style.color = 'white'; button.style.border = 'none'; button.style.padding = '5px 10px'; button.style.borderRadius = '3px'; button.style.cursor = 'pointer'; button.addEventListener('click', () => { const apiKey = input.value.trim(); if (!apiKey) { alert('Please enter a valid API key.'); return; } setApiKey(apiKey); document.body.removeChild(toast); }); toast.appendChild(button); document.body.appendChild(toast); }; // Function to detect URL changes and re-add buttons let currentUrl = window.location.href; const observeUrlChanges = () => { setInterval(() => { if (window.location.href !== currentUrl) { currentUrl = window.location.href; setTimeout(() => { addButtons(); }, 1000); // 1.5 seconds delay to re-add buttons } }, 250); // Check URL every 250 milliseconds }; // Start observing URL changes observeUrlChanges(); setTimeout(() => addButtons(), 2500); })();