Greasy Fork镜像 is available in English.

Torn Forum Timestamps Highlighter with Local Storage

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);
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。