Gelbooru 无限滚动

实现 Gelbooru 图片浏览页面无限滚动,支持 post、pool、favorites 以及 saved search 页

As of 2022-01-31. See the latest version.

// ==UserScript==
// @name         Gelbooru 无限滚动
// @namespace    https://greasyfork.org/zh-CN/scripts/439308
// @version      0.7
// @description  实现 Gelbooru 图片浏览页面无限滚动,支持 post、pool、favorites 以及 saved search 页
// @author       ctrn43062
// @include      *://gelbooru.com/index.php*
// @icon         https://gelbooru.com/favicon.ico
// @grant        none
// @license MIT

// ==/UserScript==

// 是否正在加载下一页数据 flag
let loadingNextPage = false;

// 是否显示分页栏
const showPaginator = true;

// 当滚动条距离页面底部多少像素的时候触发加载下一页数据功能;调大这个值以更提前地加载下一页数据
const scrollBuffer = 500;

const baseURLObj = new URL(location.href);
const searchParams = baseURLObj.searchParams;

let currentPage = parseInt(searchParams.get('pid')) || 0;

// Gelbooru 的 pid 增长值(无需修改)
let pageSize = 42;
// 获取页面的类型,目前支持的页面有 post pool favorites
// 不同的页面的 pageSize 和 html 结构可能有所不同
const pageType = searchParams.get('page');

// 页面有 s=view 表示正在查看单图,无需显示加载提示
const showTip = !(searchParams.get('s') === 'view');

// 因为 favorite 页的 html 结构不同所以需要特判当前页面是否是 favorite 页
// 这个列表维护所有无需特判的页
const PAGES_WITHOUT_FAVORITE = ['post', 'pool'];

// 获取数据
function getPage() {
    const url = baseURLObj.origin + baseURLObj.pathname + '?' + searchParams;
    return fetch(url).then(resp => resp.text())
}

function getPageSize() {
    switch (pageType) {
        case 'post':
            return 42;
        case 'pool':
            return 45;
        case 'favorites':
            return 50;
        case 'tags':
            return 5;
    }
}

// 显示页面加载提示
function showLoadingTip(tip = '') {
    if (!showTip) {
        return {
            remove: () => {}
        }
    }

    const loadingTip = document.createElement('center');
    const paginator = document.querySelector('#paginator')
    const pagination = paginator.parentElement;

    // 加载下一页数据的提示文本
    loadingTip.textContent = tip || `Loading Page ${Math.floor(currentPage / pageSize) + 1}`
    loadingTip.style.margin = '30px 0';
    loadingTip.style.color = '#888';
    loadingTip.style.fontSize = '0.85rem';
    loadingTip.style.fontWeight = 'bold';
    loadingTip.style.clear = 'both';

    pagination.insertBefore(loadingTip, paginator);

    return loadingTip;
}

function getScrollHeight() {
    const scrollTop =
        document.documentElement.scrollTop === 0 ? document.body.scrollTop : document.documentElement.scrollTop;

    const scrollHeight =
        document.documentElement.scrollTop === 0 ? document.body.scrollHeight : document.documentElement.scrollHeight;

    const innerHeight = window.innerHeight;

    return {
        scrollTop: scrollTop,
        scrollHeight: scrollHeight,
        innerHeight: innerHeight
    };
}

function addFavorite(container) {
    container.querySelectorAll('a > img[alt]').forEach(img => {
        const fav = document.createElement('center');
        const parentEle = img.parentElement;
        const href = parentEle.href;
        const postId = new URL(href).searchParams.get('id');

        fav.innerHTML = `<a href="#" onclick="post_vote('${postId}', 'up'); addFav('${postId}'); return false;">Fav</a>`;

        parentEle.style.position = 'relative';
        fav.style.position = 'absolute';
        fav.style.bottom = 0;
        fav.style.transform = 'translate(-50%, 100%)';
        fav.style.left = '50%';
        img.parentElement.appendChild(fav);
    })
}


function _addFav(a) {
    $.get("public/addfav.php?id=" + a, function(b) {
        if (b == "1") {
            window.notice(`Post <strong>${a}</strong> already in your favorites`)
        } else {
            if (b == "2") {
               window.notice("You are not logged in")
            } else {
                window.notice(`Post <strong>${a}</strong> added to favorites`)
            }
        }
    })
}


function notice() {
    let timer = null;
    const noticeEle = document.querySelector('#notice');

    return (message) => {
        noticeEle.style.display = 'block';
        noticeEle.innerHTML = `<span>${message}</span>`;

        if (timer) {
            clearTimeout(timer);
            timer = null;
            return ;
        } else {
            timer = setTimeout(() => {
                noticeEle.style.display = ' none';
                timer = null;
            }, 3000);
        }
    }
}


// 初始化基本功能
function init() {
    pageSize = getPageSize();

    if (!showPaginator) {
        const paginator = document.querySelector('#paginator');
        paginator.style.display = 'none';
    }

    // 仅支持 post 页显示快速 fav 按钮
    if (pageType === 'post') {
        addFavorite(document);
    }

    // 添加自定义 fav 函数,原本功能不变,修改的主要是 notice 的提示
    window.addFav = _addFav;

    // 添加自定义 notice
    window.notice = notice();
}

(function() {
    'use strict';
    init();

    window.addEventListener('scroll', () => {
        const scroll = getScrollHeight();
        const scrollTop = scroll['scrollTop'],
            scrollHeight = scroll['scrollHeight'],
            innerHeight = scroll['innerHeight'];

        // 当滚动条触底且没有正在加载数据的时候,加载下一页数据
        if (scrollTop + innerHeight >= scrollHeight - scrollBuffer && !loadingNextPage) {
            loadingNextPage = true;
            currentPage = parseInt(currentPage) + pageSize;
            searchParams.set('pid', currentPage);

            // 地址栏的 pid 会跟随滚动更新
            // history.replaceState({}, '', baseURLObj);

            const loadingTip = showLoadingTip();

            getPage().then(text => {
                let container, nodes;
                const isFavoritePage = !PAGES_WITHOUT_FAVORITE.includes(pageType);
                const dummyHTML = document.createElement('html');
                dummyHTML.innerHTML = text;

                const dummyPaginator = dummyHTML.querySelector('#paginator');
                document.querySelector('#paginator').innerHTML = dummyPaginator.innerHTML;

                if (!isFavoritePage) {
                    container = document.querySelector('.thumbnail-container');
                    nodes = dummyHTML.querySelector('.thumbnail-container').children;
                    if (pageType === 'post') {
                        addFavorite(dummyHTML);
                    }
                } else if (pageType === 'favorites') {
                    container = document.querySelector('#paginator');
                    nodes = dummyHTML.querySelectorAll('.thumb');
                } else if (pageType === 'tags') { // 适配 saved search 页   粪代码
                    container = document.querySelector('#paginator');
                    const node = dummyHTML.querySelector('main');
                    node.querySelector('.mainBodyPadding').remove();
                    node.querySelector('#paginator').remove();
                    nodes = node.children;
                }

                // 没有数据了
                if (nodes.length === 0) {
                    loadingTip.textContent = 'Nobody here but us chickens!';
                    return;
                }

                Array.from(nodes).forEach((node) => {
                    if (isFavoritePage) {
                        container.parentElement.insertBefore(node, container)
                    } else {
                        container.appendChild(node)
                    }
                })

                loadingNextPage = false;
                loadingTip.remove();
            })
        }
    })
})();