F95zone Development Time Tracker & Sorter

Calculates and shows the development duration of games on f95zone.to/sam/latest_alpha/* page with a sorting feature.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name             F95zone Development Time Tracker & Sorter
// @namespace        http://tampermonkey.net/
// @version          1.0
// @description      Calculates and shows the development duration of games on f95zone.to/sam/latest_alpha/* page with a sorting feature. 
// @author           0wn3dbot
// @license          MIT
// @match            https://f95zone.to/sam/latest_alpha/*
// @grant            GM_xmlhttpRequest
// @grant            GM_addStyle
// @grant            GM_setValue
// @grant            GM_getValue
// @connect          f95zone.to
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const CONFIG = {
        filterStorageKey: 'devTimeFilterSettings',
        sortStorageKey: 'devTimeSortEnabled',
        requestBatchSize: 5, // Number of requests to send simultaneously
        creationDateStoragePrefix: 'threadCreationDate_' // Prefix for keys in GM storage
    };

    // --- Styles ---
    GM_addStyle(`
        /* Main overlay styles */
        .resource-tile {
            position: relative;
            overflow: hidden;
            transition: transform 0.3s ease, opacity 0.3s ease;
        }
        .resource-tile_thumb-wrap {
            position: relative;
        }
        .dev-time-overlay {
            position: absolute;
            top: 0;
            left: 0;
            background-color: rgba(15, 15, 15, 0.8);
            color: #e0e0e0;
            padding: 3px 6px;
            font-size: 11px;
            font-weight: 500;
            border-radius: 0 8px 0 0;
            z-index: 10;
            font-family: 'Lato', 'Open Sans', sans-serif;
            pointer-events: none;
            line-height: 1.2;
            box-shadow: 1px 1px 3px rgba(0,0,0,0.5);
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
            border-right: 1px solid rgba(255, 255, 255, 0.1);
            text-shadow: 1px 1px 1px rgba(0,0,0,0.7);
        }

        /* Toolbar */
        .dev-tools-panel {
            background: #2a2a2a;
            border-radius: 8px;
            padding: 12px 15px;
            margin: 0 auto 20px;
            max-width: 1200px;
            display: flex;
            align-items: center;
            flex-wrap: wrap;
            gap: 15px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.3);
            border: 1px solid #3a3a3a;
        }

        .dev-progress-container {
            flex: 1;
            min-width: 200px;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .dev-progress-bar {
            flex: 1;
            height: 8px;
            background: #3a3a3a;
            border-radius: 4px;
            overflow: hidden;
        }

        .dev-progress-fill {
            height: 100%;
            background: linear-gradient(90deg, #4a8bfc, #2a65c5);
            width: 0%;
            transition: width 0.3s ease;
        }

        .dev-progress-text {
            font-size: 12px;
            color: #aaa;
            min-width: 80px;
            text-align: center;
            font-family: 'Lato', sans-serif;
        }

        .dev-tools-btn {
            background: #3a3a3a;
            color: #ddd;
            border: none;
            padding: 6px 12px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
            font-family: 'Lato', sans-serif;
            transition: all 0.2s ease;
            display: flex;
            align-items: center;
            gap: 5px;
        }

        .dev-tools-btn:hover {
            background: #4a4a4a;
            color: #fff;
        }

        .dev-tools-btn.active {
            background: #4a8bfc;
            color: #fff;
        }

        .dev-tools-btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }

        .dev-filter-container {
            display: flex;
            align-items: center;
            gap: 5px;
            background: rgba(0,0,0,0.2);
            padding: 5px 8px;
            border-radius: 4px;
            border: 1px solid #3a3a3a;
        }

        .dev-filter-input {
            width: 30px;
            background: #3a3a3a;
            border: 1px solid #4a4a4a;
            color: #ddd;
            padding: 3px;
            text-align: center;
            border-radius: 3px;
            font-size: 11px;
        }

        .dev-filter-separator {
            color: #666;
            font-size: 12px;
        }

        /* Animations */
        @keyframes pulse {
            0% { opacity: 0.6;
            }
            50% { opacity: 1;
            }
            100% { opacity: 0.6;
            }
        }

        .dev-processing {
            animation: pulse 1.5s infinite;
        }

        .dev-hidden {
            transform: scale(0.95);
            opacity: 0.3;
            pointer-events: none;
        }
    `);

    // --- Helper functions ---

    /**
     * Converts development time string to milliseconds
     * @param {string} timeStr - Time string (e.g., "1y, 2m, 3w, 4d")
     * @returns {number} Time in milliseconds
     */
    function timeStringToMs(timeStr) {
        if (!timeStr || timeStr === "N/A") return 0;
        const msPerDay = 1000 * 60 * 60 * 24;
        const msPerWeek = msPerDay * 7;
        const msPerMonth = msPerDay * (365.25 / 12);
        const msPerYear = msPerDay * 365.25;

        let totalMs = 0;
        const parts = timeStr.split(',').map(p => p.trim());

        parts.forEach(part => {
            if (part.includes('y')) {
                const years = parseInt(part.replace('y', '')) || 0;
                totalMs += years * msPerYear;
            } else if (part.includes('m')) {
                const months = parseInt(part.replace('m', '')) || 0;
                totalMs += months * msPerMonth;
            } else if (part.includes('w')) {
                const weeks = parseInt(part.replace('w', '')) || 0;
                totalMs += weeks * msPerWeek;
            } else if (part.includes('d')) {
                const days = parseInt(part.replace('d', '')) || 0;
                totalMs += days * msPerDay;
            } else if (part === "<1d") {
                totalMs += msPerDay * 0.5; // Half a day for "<1d"
            }
        });

        return totalMs;
    }

    /**
     * Calculates the difference between two dates and formats it.
     * @param {Date} startDate - Start date (thread creation)
     * @param {Date} endDate - End date (last update)
     * @returns {string} Formatted difference string (e.g., "1y, 2m, 3w, 1d")
     */
    function calculateDateDifference(startDate, endDate) {
        if (!startDate || !endDate) {
            return "N/A";
        }

        if (startDate > endDate) {
            return ">1d"; // Display ">1d" if creation date is in the future
        }

        let diff = Math.abs(endDate - startDate);
        const msPerDay = 1000 * 60 * 60 * 24;
        const msPerWeek = msPerDay * 7;
        const msPerMonth = msPerDay * (365.25 / 12);
        const msPerYear = msPerDay * 365.25;

        const years = Math.floor(diff / msPerYear);
        diff -= years * msPerYear;

        const months = Math.floor(diff / msPerMonth);
        diff -= months * msPerMonth;
        const weeks = Math.floor(diff / msPerWeek);
        diff -= weeks * msPerWeek;

        const days = Math.floor(diff / msPerDay);
        let result = [];
        if (years > 0) result.push(`${years}y`);
        if (months > 0) result.push(`${months}m`);
        if (weeks > 0) result.push(`${weeks}w`);
        if (days > 0 && (result.length === 0 || years > 0 || months > 0 || weeks > 0)) {
            result.push(`${days}d`);
        }
        if (result.length === 0 && Math.abs(endDate - startDate) > 0) {
            return "&lt;1d";
        }

        return result.join(', ') || "N/A";
    }

    /**
     * Determines the approximate last update date based on the text in the tile.
     * @param {Element} timeElement - Span element inside .resource-tile_info-meta_time
     * @returns {Date|null} Calculated date or null
     */
    function getLastUpdateDate(timeElement) {
        const now = new Date();
        if (!timeElement) return now;

        const timeValue = parseInt(timeElement.textContent.trim(), 10);
        const timeClass = timeElement.className;
        if (timeClass.includes('tile-date_mins')) {
            now.setMinutes(now.getMinutes() - (isNaN(timeValue) ? 0 : timeValue));
        } else if (timeClass.includes('tile-date_hrs')) {
            now.setHours(now.getHours() - (isNaN(timeValue) ? 0 : timeValue));
        } else if (timeClass.includes('tile-date_yesterday')) {
            now.setDate(now.getDate() - 1);
        } else if (timeClass.includes('tile-date_days')) {
            now.setDate(now.getDate() - (isNaN(timeValue) ? 0 : timeValue));
        } else if (timeClass.includes('tile-date_week')) {
            now.setDate(now.getDate() - (isNaN(timeValue) ? 0 : timeValue) * 7);
        } else if (timeClass.includes('tile-date_month')) {
            now.setMonth(now.getMonth() - (isNaN(timeValue) ? 0 : timeValue));
        } else if (timeClass.includes('tile-date_years')) {
            now.setFullYear(now.getFullYear() - (isNaN(timeValue) ? 0 : timeValue));
        } else {
            return new Date();
        }
        return now;
    }

    /**
     * Finds the thread creation date on the page
     * @param {Document} doc - DOM of the thread page
     * @returns {Date|null} Creation date or null
     */
    function findThreadCreationDate(doc) {
        // Try to find the standard time element
        let threadTimeElement = doc.querySelector('time.u-dt[datetime]');
        if (threadTimeElement) {
            const creationDateTimeString = threadTimeElement.getAttribute('datetime');
            const creationDate = new Date(creationDateTimeString);
            return isNaN(creationDate) ? null : creationDate;
        }

        // Alternative for pages with a different format
        const startDateElement = doc.querySelector('i.fa-clock')?.nextElementSibling?.nextElementSibling;
        if (startDateElement) {
            const timeElement = startDateElement.querySelector('time.u-dt[datetime]');
            if (timeElement) {
                const creationDateTimeString = timeElement.getAttribute('datetime');
                const creationDate = new Date(creationDateTimeString);
                return isNaN(creationDate) ? null : creationDate;
            }
        }

        return null;
    }

    // --- Class for managing tools ---
    class DevTools {
        constructor() {
            this.totalTiles = 0;
            this.processedTiles = 0;
            this.tilesData = new Map();
            this.sortActive = GM_getValue(CONFIG.sortStorageKey, false);
            this.filterSettings = GM_getValue(CONFIG.filterStorageKey, { min: {}, max: {} });
            this.processingQueue = [];
            this.isProcessing = false;
            this.collectionStarted = false;

            this.initPanel();
            this.updateProgress(); // Initialize progress to display 0/0
        }

        initPanel() {
            // Create a container for the toolbar
            const noticesContainer = document.querySelector('.notices--block')?.parentElement ||
            document.body;
            this.panelElement = document.createElement('div');
            this.panelElement.className = 'dev-tools-panel';

            // "Start Collection" button
            this.startButton = document.createElement('button');
            this.startButton.className = 'dev-tools-btn';
            this.startButton.innerHTML = '<i class="fas fa-play"></i> Start Collection';
            this.startButton.addEventListener('click', () => this.startCollection());
            // Progress container (hidden until collection starts)
            this.progressContainer = document.createElement('div');
            this.progressContainer.className = 'dev-progress-container';
            this.progressContainer.style.display = 'none';

            this.progressBar = document.createElement('div');
            this.progressBar.className = 'dev-progress-bar';

            this.progressFill = document.createElement('div');
            this.progressFill.className = 'dev-progress-fill';
            this.progressBar.appendChild(this.progressFill);
            this.progressText = document.createElement('span');
            this.progressText.className = 'dev-progress-text';
            this.progressText.textContent = '0/0';

            this.progressContainer.appendChild(this.progressBar);
            this.progressContainer.appendChild(this.progressText);
            // Sort button
            this.sortBtn = document.createElement('button');
            this.sortBtn.className = `dev-tools-btn ${this.sortActive ? 'active' : ''}`;
            this.sortBtn.innerHTML = '<i class="fas fa-sort-amount-down"></i> Sort';
            this.sortBtn.disabled = true;
            this.sortBtn.addEventListener('click', () => this.toggleSort());

            // Assembling the panel
            this.panelElement.append(
                this.startButton,
                this.progressContainer,
                this.sortBtn
            );
            // Insert the panel after notifications or at the beginning of the body
            noticesContainer.insertBefore(this.panelElement, noticesContainer.firstChild);
        }

        startCollection() {
            if (!this.collectionStarted) {
                this.collectionStarted = true;
                this.startButton.style.display = 'none';
                this.progressContainer.style.display = 'flex';
                this.totalTiles = document.querySelectorAll('.resource-tile').length; // Set the total number of tiles once
                this.updateProgress();
                this.processAllTiles();
                setTimeout(() => this.observeDOM(), 10000); // Start MutationObserver after 10 seconds
                this.startPeriodicCheck();
            }
        }

        updateProgress() {
            this.progressText.textContent = `${this.processedTiles}/${this.totalTiles}`;
            const progressPercent = this.totalTiles > 0 ? (this.processedTiles / this.totalTiles) * 100 : 0;
            this.progressFill.style.width = `${progressPercent}%`;
            // Activate buttons if all tiles are processed
            if (this.processedTiles > 0 && this.processedTiles === this.totalTiles) {
                this.sortBtn.disabled = false;

                // Apply saved sorting
                if (this.sortActive) {
                    this.applySort();
                }
            }
        }

        toggleSort() {
            this.sortActive = !this.sortActive;
            this.sortBtn.classList.toggle('active', this.sortActive);
            GM_setValue(CONFIG.sortStorageKey, this.sortActive);

            if (this.sortActive) {
                this.applySort();
            } else {
                this.resetSort();
            }
        }

        applySort() {
            const container = document.querySelector('#latest-page_items-wrap_inner');
            if (!container) return;

            // Create an array of tiles for sorting
            const tilesArray = Array.from(this.tilesData.keys());
            // Sort by development time (descending), placing ">1d" at the end
            tilesArray.sort((a, b) => {
                const aData = this.tilesData.get(a);
                const bData = this.tilesData.get(b);

                if (aData.devTime === ">1d" && bData.devTime !== ">1d") {
                    return 1;
                }
                if (bData.devTime === ">1d" && aData.devTime !== ">1d") {
                    return -1;
                }

                const aMs = timeStringToMs(aData.devTime);
                const bMs = timeStringToMs(bData.devTime);

                return bMs - aMs;
            });
            // Rearrange tiles in the DOM
            tilesArray.forEach(tile => {
                container.appendChild(tile);
            });
        }

        resetSort() {
            const container = document.querySelector('#latest-page_items-wrap_inner');
            if (!container) return;

            // Create an array of tiles with their original positions
            const tilesArray = Array.from(this.tilesData.keys()).map(tile => ({
                tile,
                originalOrder: parseInt(tile.dataset.originalOrder || '0')
            }));
            // Sort by original order
            tilesArray.sort((a, b) => a.originalOrder - b.originalOrder);
            // Rearrange tiles in the DOM
            tilesArray.forEach(({tile}) => {
                container.appendChild(tile);
            });
        }

        addTileToQueue(tile, threadUrl, lastUpdateDate) {
            if (!tile.dataset.devTimeProcessed) {
                tile.dataset.devTimeProcessed = 'pending';
                this.processingQueue.push({ tile, threadUrl, lastUpdateDate });
                this.updateProgress(); // Update progress to show the total count
                if (!this.isProcessing) {
                    this.processQueue();
                }
            }
        }

        processQueue() {
            if (this.processingQueue.length === 0) {
                this.isProcessing = false;
                return;
            }

            this.isProcessing = true;
            const batch = this.processingQueue.splice(0, CONFIG.requestBatchSize);
            const requests = batch.map(({ tile, threadUrl, lastUpdateDate }) => {
                const threadIdMatch = threadUrl.match(/threads\/(\d+)/);
                const threadId = threadIdMatch ? threadIdMatch[1] : null;
                const creationDateKey = threadId ? `${CONFIG.creationDateStoragePrefix}${threadId}` : null;

                return new Promise((resolve, reject) => {
                    if (creationDateKey) {
                        const storedCreationDate = GM_getValue(creationDateKey);
                        if (storedCreationDate) {
                            const creationDate = new Date(storedCreationDate);
                            const diffString = calculateDateDifference(creationDate, lastUpdateDate);
                            const thumbWrap = tile.querySelector('.resource-tile_thumb-wrap');
                            if (thumbWrap) {
                                const overlay = document.createElement('div');
                                overlay.className = 'dev-time-overlay';
                                overlay.innerHTML = `Dev Time: ${diffString}`;
                                thumbWrap.appendChild(overlay);
                            }
                            devTools.tilesData.set(tile, { devTime: diffString, lastUpdateDate, creationDate });
                            tile.dataset.devTimeProcessed = 'true';
                            devTools.processedTiles++;
                            devTools.updateProgress();
                            resolve();
                            return;
                        }
                    }

                    GM_xmlhttpRequest({
                        method: "GET",
                        url: threadUrl,
                        onload: function(response) {
                            if (response.status >= 200 && response.status < 300) {
                                try {
                                    const parser = new DOMParser();
                                    const doc = parser.parseFromString(response.responseText, "text/html");
                                    const creationDate = findThreadCreationDate(doc);

                                    if (creationDate) {
                                        if (creationDateKey) {
                                            GM_setValue(creationDateKey, creationDate.toISOString());
                                        }
                                        const diffString = calculateDateDifference(creationDate, lastUpdateDate);
                                        const thumbWrap = tile.querySelector('.resource-tile_thumb-wrap');
                                        if (thumbWrap) {
                                            const overlay = document.createElement('div');
                                            overlay.className = 'dev-time-overlay';
                                            overlay.innerHTML = `Dev Time: ${diffString}`;
                                            thumbWrap.appendChild(overlay);
                                        }
                                        devTools.tilesData.set(tile, { devTime: diffString, lastUpdateDate, creationDate });
                                        tile.dataset.devTimeProcessed = 'true';
                                        devTools.processedTiles++;
                                        devTools.updateProgress();
                                        resolve();
                                    } else {
                                        console.warn(`Could not find creation date on: ${threadUrl}`);
                                        devTools.tilesData.set(tile, { devTime: "N/A", lastUpdateDate });
                                        tile.dataset.devTimeProcessed = 'failed';
                                        devTools.processedTiles++;
                                        devTools.updateProgress();
                                        resolve();
                                    }
                                } catch (e) {
                                    console.error(`Error parsing response for ${threadUrl}:`, e);
                                    devTools.tilesData.set(tile, { devTime: "N/A", lastUpdateDate });
                                    tile.dataset.devTimeProcessed = 'error';
                                    devTools.processedTiles++;
                                    devTools.updateProgress();
                                    resolve();
                                }
                            } else {
                                console.error(`Failed to fetch ${threadUrl}. Status: ${response.status}`);
                                devTools.tilesData.set(tile, { devTime: "N/A", lastUpdateDate });
                                tile.dataset.devTimeProcessed = 'failed';
                                devTools.processedTiles++;
                                devTools.updateProgress();
                                resolve();
                            }
                        },
                        onerror: function(error) {
                            console.error(`Network error fetching ${threadUrl}:`, error);
                            devTools.tilesData.set(tile, { devTime: "N/A", lastUpdateDate });
                            tile.dataset.devTimeProcessed = 'error';
                            devTools.processedTiles++;
                            devTools.updateProgress();
                            resolve();
                        }
                    });
                });
            });

            Promise.all(requests).then(() => {
                setTimeout(() => this.processQueue(), 200); // Small delay between batches
            });
        }

        processAllTiles() {
            const tiles = document.querySelectorAll('.resource-tile:not([data-dev-time-processed])');
            // totalTiles is set in startCollection once
            tiles.forEach(tile => {
                if (!tile.dataset.originalOrder) {
                    tile.dataset.originalOrder = Array.from(tile.parentNode.children).indexOf(tile);
                }
                const linkElement = tile.querySelector('a.resource-tile_link');
                const timeMetaElement = tile.querySelector('.resource-tile_info-meta_time span');

                if (!linkElement) {
                    console.warn('Could not find link for tile:', tile);
                    return;
                }

                const threadUrl = linkElement.href;
                const lastUpdateDate = getLastUpdateDate(timeMetaElement);

                const threadIdMatch = threadUrl.match(/threads\/(\d+)/);
                const threadId = threadIdMatch ? threadIdMatch[1] : null;
                const creationDateKey = threadId ? `${CONFIG.creationDateStoragePrefix}${threadId}` : null;

                if (creationDateKey && GM_getValue(creationDateKey)) {
                    const storedCreationDate = GM_getValue(creationDateKey);
                    const creationDate = new Date(storedCreationDate);
                    const diffString = calculateDateDifference(creationDate, lastUpdateDate);
                    const thumbWrap = tile.querySelector('.resource-tile_thumb-wrap');
                    if (thumbWrap) {
                        const overlay = document.createElement('div');
                        overlay.className = 'dev-time-overlay';
                        overlay.innerHTML = `Dev Time: ${diffString}`;
                        thumbWrap.appendChild(overlay);
                    }
                    devTools.tilesData.set(tile, { devTime: diffString, lastUpdateDate, creationDate });
                    tile.dataset.devTimeProcessed = 'true';
                    devTools.processedTiles++;
                    devTools.updateProgress();
                } else {
                    this.addTileToQueue(tile, threadUrl, lastUpdateDate);
                }
            });
        }

        observeDOM() {
            const observerTarget = document.querySelector('#latest-page_items-wrap_inner');
            if (observerTarget) {
                const observer = new MutationObserver(mutations => {
                    mutations.forEach(mutation => {
                        if (mutation.addedNodes && mutation.addedNodes.length > 0) {
                            mutation.addedNodes.forEach(node => {
                                if (node.nodeType === Node.ELEMENT_NODE) {
                                    if (node.matches('.resource-tile')) {
                                        this.processTile(node);
                                    } else {
                                        const newTiles = node.querySelectorAll('.resource-tile:not([data-dev-time-processed])');
                                        newTiles.forEach(this.processTile.bind(this));
                                    }
                                }
                            });
                        }
                    });
                });
                observer.observe(observerTarget, {
                    childList: true,
                    subtree: true
                });
            } else {
                console.error("Target element for MutationObserver (#latest-page_items-wrap_inner) not found.");
            }
        }

        processTile(tile) {
            if (!tile || tile.dataset.devTimeProcessed) {
                return;
            }
            if (!tile.dataset.originalOrder) {
                tile.dataset.originalOrder = Array.from(tile.parentNode.children).indexOf(tile);
            }

            const linkElement = tile.querySelector('a.resource-tile_link');
            const timeMetaElement = tile.querySelector('.resource-tile_info-meta_time span');

            if (!linkElement) {
                console.warn('Could not find link for tile:', tile);
                return;
            }

            const threadUrl = linkElement.href;
            const lastUpdateDate = getLastUpdateDate(timeMetaElement);

            const threadIdMatch = threadUrl.match(/threads\/(\d+)/);
            const threadId = threadIdMatch ? threadIdMatch[1] : null;
            const creationDateKey = threadId ? `${CONFIG.creationDateStoragePrefix}${threadId}` : null;

            if (creationDateKey && GM_getValue(creationDateKey)) {
                const storedCreationDate = GM_getValue(creationDateKey);
                const creationDate = new Date(storedCreationDate);
                const diffString = calculateDateDifference(creationDate, lastUpdateDate);
                const thumbWrap = tile.querySelector('.resource-tile_thumb-wrap');
                if (thumbWrap) {
                    const overlay = document.createElement('div');
                    overlay.className = 'dev-time-overlay';
                    overlay.innerHTML = `Dev Time: ${diffString}`;
                    thumbWrap.appendChild(overlay);
                }
                devTools.tilesData.set(tile, { devTime: diffString, lastUpdateDate, creationDate });
                tile.dataset.devTimeProcessed = 'true';
                devTools.processedTiles++;
                devTools.updateProgress();
            } else {
                this.addTileToQueue(tile, threadUrl, lastUpdateDate);
            }
        }

        startPeriodicCheck() {
            this.checkInterval = setInterval(() => {
                const unprocessed = document.querySelectorAll('.resource-tile:not([data-dev-time-processed])');
                if (unprocessed.length > 0) {
                    unprocessed.forEach(this.processTile.bind(this));
                }
            }, 1000);
        }
    }

    // --- Main logic ---
    const devTools = new DevTools();
})();