JavDB Infinite Scroll

提取自 javdb_infinite_scroll.user.js 的無限滾動功能,適用於 JavDB 網站

当前为 2025-03-08 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         JavDB Infinite Scroll
// @namespace    https://sleazyfork.org/
// @version      1.0.0
// @license      MIT
// @description  提取自 javdb_infinite_scroll.user.js 的無限滾動功能,適用於 JavDB 網站
// @author       Roo (原作者 Hobby)
// @include      *://*javdb*.com/*
// @require      https://lib.baomitu.com/jquery/2.2.4/jquery.min.js
// @grant        GM_addStyle
// @grant        GM_getValue
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    /**
     * 多线程异步队列 依赖 jQuery 1.8+
     * @param {Number} n 正整数, 线程数量
     */
    function Queue(n) {
        n = parseInt(n, 10);
        return new Queue.prototype.init((n && n > 0) ? n : 1);
    }

    Queue.prototype = {
        init: function (n) {
            this.threads = [];
            this.taskList = [];
            while (n--) {
                this.threads.push(new this.Thread());
            }
        },
        push: function (callback) {
            if (typeof callback !== 'function') return;
            var index = this.indexOfIdle();
            if (index != -1) {
                this.threads[index].idle(callback);
            } else {
                this.taskList.push(callback);
                for (var i = 0, l = this.threads.length; i < l; i++) {
                    ((thread, self, id) => {
                        thread.idle(() => {
                            if (self.taskList.length > 0) {
                                let promise = self.taskList.shift()();
                                return promise.promise ? promise : $.Deferred().resolve().promise();
                            } else {
                                return $.Deferred().resolve().promise();
                            }
                        });
                    })(this.threads[i], this, i);
                }
            }
        },
        indexOfIdle: function () {
            var threads = this.threads,
                thread = null,
                index = -1;
            for (var i = 0, l = threads.length; i < l; i++) {
                thread = threads[i];
                if (thread.promise.state() === 'resolved') {
                    index = i;
                    break;
                }
            }
            return index;
        },
        Thread: function () {
            this.promise = $.Deferred().resolve().promise();
            this.idle = (callback) => {
                this.promise = this.promise.then(callback);
            };
        }
    };
    Queue.prototype.init.prototype = Queue.prototype;

    var thirdparty = {
        waterfallScrollInit: () => {
            var w = new thirdparty.waterfall({});
            var $pages4 = $('.movie-list.v.cols-4.vcols-8 .item, .movie-list.v.cols-4.vcols-5 .item, .movie-list.h.cols-4.vcols-8 .item, .movie-list.h.cols-4.vcols-5 .item');
            if ($pages4.length) {
                GM_addStyle(`
                    .container {max-width: inherit !important;}
                    .tags {display: block !important;}
                    .tag.hobby {display: block; float: right; color: #fff; line-height: 2em;}
                `);
                $pages4[0].parentElement.id = "waterfall";
                w = new thirdparty.waterfall({
                    next: '.pagination .pagination-next',
                    item: '.movie-list.v.cols-4.vcols-8 .item, .movie-list.v.cols-4.vcols-5 .item, .movie-list.h.cols-4.vcols-8 .item, .movie-list.h.cols-4.vcols-5 .item',
                    cont: '#waterfall',
                    pagi: '.pagination',
                });
            }
            if (GM_getValue('scroll_status', 1) > 0) {
                document.addEventListener('scroll', w.scroll.bind(w));
                document.addEventListener('wheel', w.wheel.bind(w));
            }
        },
        waterfall: function (selectorcfg = {}) {
            class Lock {
                constructor(d = false) {
                    this.locked = d;
                }
                lock() {
                    this.locked = true;
                }
                unlock() {
                    this.locked = false;
                }
            }
            this.queue = new Queue(1);
            this.page_queue = new Queue(1);
            this.lock = new Lock();
            this.baseURI = this.getBaseURI();
            this.selector = {
                next: 'a.next',
                item: '',
                cont: '#waterfall',
                pagi: '.pagination',
            };
            Object.assign(this.selector, selectorcfg);
            this.pagegen = this.fetchSync(location.href);
            this.anchor = $(this.selector.pagi)[0];
            this._count = 0;
            this._1func = (cont, elems) => {
                cont.empty().append(elems);
            };
            this._2func = (cont, elems) => {
                cont.append(elems);
            };
            if ($(this.selector.item).length) {
                this.appendElems(this._1func);
            }
        }
    };

    thirdparty.waterfall.prototype.getBaseURI = function () {
        let _ = location;
        return `${_.protocol}//${_.hostname}${(_.port && `:${_.port}`)}`;
    };
    thirdparty.waterfall.prototype.getNextURL = function (href) {
        let a = document.createElement('a');
        a.href = href;
        return `${this.baseURI}${a.pathname}${a.search}`;
    };
    thirdparty.waterfall.prototype.fetchURL = function (url) {
        console.log(`fetchUrl = ${url}`);
        let status = 404;
        const fetchwithcookie = fetch(url, { credentials: 'same-origin' });
        return fetchwithcookie.then(response => {
            status = response.status;
            return response.text();
        }).then(html => new DOMParser().parseFromString(html, 'text/html'))
        .then(doc => {
            let $doc = $(doc);
            let elems = [];
            let nextURL;
            if (status < 300) {
                let href = $doc.find(this.selector.next).attr('href');
                nextURL = href ? this.getNextURL(href) : undefined;
                elems = $doc.find(this.selector.item);
                for (const elem of elems) {
                    const links = elem.getElementsByTagName('a');
                    for (const link of links) {
                        link.target = "_blank";
                    }
                }
                if ($(this.selector.item).length && (this._count !== 0) && url === nextURL) {
                    if ($(`#waterfall>div>a[href="${$(elems[0]).find('a.box')[0].attr('href')}"]`).length > 0) {
                        nextURL = undefined;
                        elems = [];
                    }
                }
            } else {
                nextURL = $doc.url;
            }
            return { nextURL, elems };
        });
    };
    thirdparty.waterfall.prototype.fetchSync = function* (urli) {
        let url = urli;
        do {
            yield new Promise((resolve, reject) => {
                if (this.lock.locked) {
                    reject();
                } else {
                    this.lock.lock();
                    resolve();
                }
            }).then(() => {
                return this.fetchURL(url).then(info => {
                    url = info.nextURL;
                    return info.elems;
                });
            }).then(elems => {
                this.lock.unlock();
                return elems;
            }).catch(() => {});
        } while (url);
    };
    thirdparty.waterfall.prototype.appendElems = function () {
        let nextpage = this.pagegen.next();
        if (!nextpage.done) {
            nextpage.value.then(elems => {
                const cb = (this._count === 0) ? this._1func : this._2func;
                cb($(this.selector.cont), elems);
                this._count += 1;
            });
        }
        return nextpage.done;
    };
    thirdparty.waterfall.prototype.end = function () {
        document.removeEventListener('scroll', this.scroll.bind(this));
        document.removeEventListener('wheel', this.wheel.bind(this));
        let $end = $(`<h1>The End</h1>`);
        $(this.anchor).replaceWith($end);
    };
    thirdparty.waterfall.prototype.reachBottom = function (elem, limit) {
        return (elem.getBoundingClientRect().top - $(window).height()) < limit;
    };
    thirdparty.waterfall.prototype.scroll = function () {
        this.pageQueuePush();
    };
    thirdparty.waterfall.prototype.wheel = function () {
        this.pageQueuePush();
    };
    thirdparty.waterfall.prototype.pageQueuePush = function () {
        this.page_queue.push(() => {
            let defer = $.Deferred();
            new Promise(resolve => {
                if (this.reachBottom(this.anchor, 1200) && this.appendElems(this._2func)) {
                    this.end();
                }
                resolve();
            }).then(() => {
                setTimeout(() => {
                    defer.resolve();
                }, 500);
            });
            return defer.promise();
        });
    };

    thirdparty.waterfallScrollInit();
})();