Save Exhentai's reading status

Add a button that saves Exhentai's reading status by saving the time pressed and highlighting the closest gallery

// ==UserScript==
// @name         Save Exhentai's reading status
// @namespace    http://tampermonkey.net/
// @version      0.8
// @description  Add a button that saves Exhentai's reading status by saving the time pressed and highlighting the closest gallery
// @author       megu10
// @match       https://exhentai.org/
// @grant        none
// ==/UserScript==

// ==UserScript==
// @name         Save Exhentai's reading status
// @namespace    http://tampermonkey.net/
// @version      0.7
// @description  Add a button that saves Exhentai's reading status by saving the time pressed and highlighting the closest gallery
// @author       megu10
// @match       https://exhentai.org/
// @grant        none
// ==/UserScript==
// Wrap the entire script in an IIFE to avoid polluting the global scope.
// Wrap the entire script in an IIFE to avoid polluting the global scope.
(function() {
    'use strict';

    // --- State Variable ---
    // To hold the ID of the setInterval so we can stop it.
    let scrollIntervalId = null;

    // --- 1. UI Setup ---
    const topPane = document.getElementById("toppane");
    if (!topPane) {
        console.error("Save Progress Script: Could not find the '#toppane' element.");
        return;
    }
    const container = document.createElement("div");
    container.style.position = "relative";
    container.style.left = "30px";
    container.style.display = "flex";
    container.style.alignItems = "center";
    container.style.gap = "15px";

    // "Save Progress" Button
    const saveButton = document.createElement("button");
    saveButton.textContent = "Save Progress";
    Object.assign(saveButton.style, {
        backgroundColor: "#34353b",
        color: "#f1f1f1",
        fontWeight: 'bold',
        borderColor: "transparent",
        fontFamily: "Arial",
        cursor: "pointer",
        padding: "5px 10px"
    });

    // ** NEW: "Scroll to Last" Button **
    const scrollButton = document.createElement("button");
    scrollButton.textContent = "Scroll to Last";
    Object.assign(scrollButton.style, {
        backgroundColor: "#555",
        color: "#f1f1f1",
        fontWeight: 'bold',
        borderColor: "transparent",
        fontFamily: "Arial",
        cursor: "pointer",
        padding: "5px 10px"
    });

    const timeText = document.createElement("div");
    timeText.textContent = "No saved time";
    Object.assign(timeText.style, {
        fontSize: "20px",
        fontFamily: "Arial"
    });

    container.appendChild(saveButton);
    container.appendChild(scrollButton); // Add the new button to the container
    container.appendChild(timeText);
    topPane.appendChild(container);


    // --- 2. Core Functions ---

    function getFormattedUTCTime() {
        const now = new Date();
        const month = String(now.getUTCMonth() + 1).padStart(2, '0');
        const day = String(now.getUTCDate()).padStart(2, '0');
        const hours = String(now.getUTCHours()).padStart(2, '0');
        const minutes = String(now.getUTCMinutes()).padStart(2, '0');
        return `${month}-${day} ${hours}:${minutes}`;
    }

    function highlightLastViewed() {
        const savedTimeStr = localStorage.getItem('saved_time_text');
        if (!savedTimeStr) return null;

        const oldMarker = document.querySelector('.last-viewed-marker');
        if (oldMarker) {
            oldMarker.classList.remove('last-viewed-marker');
            oldMarker.style.backgroundColor = '';
        }

        const galleries = document.querySelectorAll('.gl1t');
        if (galleries.length === 0) return null;

        const currentYear = new Date().getUTCFullYear();
        const savedDateTime = new Date(`${currentYear}-${savedTimeStr}Z`);

        for (const gallery of galleries) {
            const timeElement = gallery.querySelector('.gl5t div div[id^="posted_"]');
            if (timeElement) {
                const galleryTime = new Date(timeElement.textContent + 'Z');
                if (galleryTime < savedDateTime) {
                    gallery.style.backgroundColor = '#8e3424';
                    gallery.classList.add('last-viewed-marker');
                    return gallery;
                }
            }
        }
        return null;
    }

    function scrollToMarker(markerElement) {
        if (!markerElement) return;
        markerElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }

    // ** NEW: Function to stop the scrolling search process **
    function stopScrollingSearch() {
        if (scrollIntervalId) {
            clearInterval(scrollIntervalId);
            scrollIntervalId = null;
            scrollButton.textContent = "Scroll to Last";
            scrollButton.disabled = false;
            console.log("Scrolling search stopped.");
            // Remove the listeners that were added to detect user interruption
            window.removeEventListener('wheel', stopScrollingSearch);
            window.removeEventListener('touchstart', stopScrollingSearch);
        }
    }

    // ** NEW: Function to start searching and scrolling **
    function startScrollingSearch() {
        if (scrollIntervalId) return; // Already searching, do nothing.

        scrollButton.textContent = "Searching... (Scroll to Stop)";
        scrollButton.disabled = true;

        // Add listeners to allow the user to cancel the scroll
        window.addEventListener('wheel', stopScrollingSearch, { once: true });
        window.addEventListener('touchstart', stopScrollingSearch, { once: true });

        scrollIntervalId = setInterval(() => {
            const marker = highlightLastViewed();
            if (marker) {
                // Found it!
                console.log("Marker found. Scrolling into view.");
                scrollToMarker(marker);
                stopScrollingSearch();
            } else {
                // Not found yet, scroll down to load more content
                window.scrollBy(0, window.innerHeight * 0.8); // Scroll by 80% of the viewport height
            }
        }, 500); // Check every 500ms
    }


    // --- 3. Event Handlers and Initialization ---

    // Load saved time on script start and set initial button state
    const savedTime = localStorage.getItem('saved_time_text');
    if (savedTime) {
        timeText.textContent = savedTime;
        scrollButton.disabled = false;
    } else {
        scrollButton.disabled = true;
        scrollButton.style.cursor = 'not-allowed';
    }

    // Run the highlight function once on initial page load to mark any visible items
    highlightLastViewed();

    // Listen for "Save Progress" button clicks
    saveButton.addEventListener("click", function() {
        const newTime = getFormattedUTCTime();
        localStorage.setItem('saved_time_text', newTime);
        timeText.textContent = newTime;
        highlightLastViewed();
        // Enable the scroll button now that a time is saved
        scrollButton.disabled = false;
        scrollButton.style.cursor = 'pointer';
    });

    // ** NEW: Listen for "Scroll to Last" button clicks **
    scrollButton.addEventListener("click", startScrollingSearch);

    // The mutation observer now only has one job: apply the highlight to new content.
    const observer = new MutationObserver(function() {
        highlightLastViewed();
    });

    observer.observe(document.body, { childList: true, subtree: true });

})();