Safebooru Endless Scroll

Adds endless scrolling to Safebooru

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Safebooru Endless Scroll
// @namespace    https://github.com/maikgr/SafebooruEndlessScroll
// @version      1.0
// @description  Adds endless scrolling to Safebooru
// @author       maikgr
// @match        https://safebooru.org/index.php?page=post&s=list*
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let loading = false;
    let currentPage = 0;
    let noMorePosts = false;

    // Parse current page number from URL
    function getCurrentPage() {
        const urlParams = new URLSearchParams(window.location.search);
        return parseInt(urlParams.get('pid')) || 0;
    }

    // Initialize
    currentPage = getCurrentPage();

    // Create floating loading indicator for auto-scroll
    function createLoadingIndicator() {
        const indicator = document.createElement('div');
        indicator.id = 'scroll-loading-indicator';
        indicator.textContent = 'Loading more posts...';
        indicator.style.cssText = `
            position: fixed;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            padding: 12px 24px;
            background-color: rgba(0, 0, 0, 0.8);
            color: white;
            border-radius: 8px;
            font-size: 14px;
            font-weight: bold;
            z-index: 9999;
            display: none;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
        `;
        document.body.appendChild(indicator);
        return indicator;
    }

    // Create load more button
    function createLoadButton() {
        const button = document.createElement('button');
        button.id = 'load-more-btn';
        button.textContent = 'Load More Posts';
        button.style.cssText = `
            display: block;
            margin: 20px auto;
            padding: 12px 24px;
            font-size: 16px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
            transition: background-color 0.2s;
        `;
        button.onclick = loadMorePosts;
        return button;
    }

    // Update button state and appearance
    function setButtonState(state) {
        switch (state) {
            case 'loading':
                loadButton.textContent = 'Loading...';
                loadButton.disabled = true;
                loadButton.style.backgroundColor = '#999';
                loadButton.style.cursor = 'not-allowed';
                break;
            case 'ready':
                loadButton.textContent = 'Load More Posts';
                loadButton.disabled = false;
                loadButton.style.backgroundColor = '#4CAF50';
                loadButton.style.cursor = 'pointer';
                break;
            case 'error':
                loadButton.textContent = 'Error - Click to Retry';
                loadButton.disabled = false;
                loadButton.style.backgroundColor = '#e74c3c';
                loadButton.style.cursor = 'pointer';
                break;
            case 'done':
                loadButton.textContent = 'No More Posts';
                loadButton.disabled = true;
                loadButton.style.backgroundColor = '#999';
                loadButton.style.cursor = 'not-allowed';
                break;
        }
    }

    // Find the image list container
    const imageList = document.querySelector('div.image-list');
    if (!imageList) {
        console.error('Image list not found');
        return;
    }

    // Find the paginator to insert button before it
    const paginator = document.querySelector('div#paginator');
    if (!paginator) {
        console.error('Paginator not found');
        return;
    }

    // Hide the original paginator
    paginator.style.display = 'none';

    // Insert load more button after the image list
    const loadButton = createLoadButton();
    paginator.parentNode.insertBefore(loadButton, paginator);

    // Create floating loading indicator
    const loadingIndicator = createLoadingIndicator();

    // Load more posts
    async function loadMorePosts() {
        if (loading || noMorePosts) return;

        loading = true;
        setButtonState('loading');
        loadingIndicator.style.display = 'block';

        const nextPage = currentPage + 42; // Safebooru uses pid increments of 42 (42 posts per page)

        try {
            const urlParams = new URLSearchParams(window.location.search);
            urlParams.set('pid', nextPage);
            const newUrl = `${window.location.pathname}?${urlParams.toString()}`;

            const response = await fetch(newUrl);

            // Check for HTTP errors
            if (!response.ok) {
                throw new Error(`HTTP error: ${response.status}`);
            }

            const html = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');

            const newImageList = doc.querySelector('div.image-list');

            if (newImageList && newImageList.innerHTML.trim()) {
                // Check if we got actual posts (look for span.thumb)
                const newPosts = newImageList.querySelectorAll('span.thumb');

                if (newPosts.length === 0) {
                    noMorePosts = true;
                    setButtonState('done');
                    return;
                }

                // Append new posts to the image list (using insertAdjacentHTML for performance)
                imageList.insertAdjacentHTML('beforeend', newImageList.innerHTML);

                // Update currentPage only after successful load
                currentPage = nextPage;

                setButtonState('ready');
            } else {
                noMorePosts = true;
                setButtonState('done');
            }
        } catch (error) {
            console.error('Error loading more posts:', error);
            setButtonState('error');
        } finally {
            loading = false;
            loadingIndicator.style.display = 'none';
        }
    }

    // Add hover effect only when button is interactive
    loadButton.addEventListener('mouseenter', () => {
        if (!loadButton.disabled) {
            loadButton.style.backgroundColor = '#45a049';
        }
    });
    loadButton.addEventListener('mouseleave', () => {
        if (!loadButton.disabled) {
            loadButton.style.backgroundColor = '#4CAF50';
        }
    });

    // Create a sentinel element to observe for infinite scroll
    const sentinel = document.createElement('div');
    sentinel.id = 'scroll-sentinel';
    sentinel.style.height = '1px';
    paginator.parentNode.insertBefore(sentinel, loadButton);

    // Use Intersection Observer for reliable scroll detection
    const observerOptions = {
        root: null, // viewport
        rootMargin: '500px', // trigger 500px before element is visible
        threshold: 0
    };

    const scrollObserver = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting && !loading && !noMorePosts) {
                loadMorePosts();
            }
        });
    }, observerOptions);

    scrollObserver.observe(sentinel);

    console.log('Safebooru Endless Scroll loaded!');
})();