微信读书阅读时长挂机

进入读书页面后通过鼠标右键菜单开启/停止挂机。挂机操作:(1)自动向下滚动;(2)滚动到底时自动翻下一页/下一章。

// ==UserScript==
// @name         微信读书阅读时长挂机
// @namespace    https://imkero.net/
// @version      1.0
// @description  进入读书页面后通过鼠标右键菜单开启/停止挂机。挂机操作:(1)自动向下滚动;(2)滚动到底时自动翻下一页/下一章。
// @author       电脑星人
// @license      MIT
// @match        https://weread.qq.com/web/reader/*
// @icon         https://rescdn.qqmail.com/node/wr/wrpage/style/images/independent/appleTouchIcon/apple-touch-icon-144x144.png
// @grant        GM_registerMenuCommand
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // 一次向下滚动的距离
    const SCROLL_Y_DELTA = 100;
    // 翻下一页/下一章后的等待时长
    const NEXT_CHAPTER_DELAY = 10000;
    // 滚动操作最小间隔
    const SCROLL_INTERVAL_MIN = 1000;
    // 滚动操作最大间隔
    const SCROLL_INTERVAL_MAX = 6000;

    const delay = (ms) => new Promise((resolve) => {
        setTimeout(resolve, ms);
    });

    // 判断是否滚动到底
    const isPageScrolledToBottom = () => {
        const documentHeight = Math.max(
            document.documentElement.scrollHeight,
            document.documentElement.offsetHeight,
            document.documentElement.clientHeight
        );

        const scrollPosition = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
        const scrollDistanceToBottom = documentHeight - (scrollPosition + window.innerHeight);
        const threshold = 50;

        return scrollDistanceToBottom <= threshold;
    };

    // 切换章节(模拟键盘右方向键)
    const nextPage = () => {
        const event = new KeyboardEvent('keydown', {
            bubbles: true,
            cancelable: true,
            key: 'ArrowRight',
            code: 'ArrowRight',
            keyCode: 39,
            charCode: 0
        });

        document.dispatchEvent(event);
    };

    const main = async (task) => {
        while (task.running) {
            if (task.endTime && Date.now() >= task.endTime) {
                task.running = false;
                break;
            }

            window.scrollBy(0, SCROLL_Y_DELTA);

            if (isPageScrolledToBottom()) {
                nextPage();
                await delay(NEXT_CHAPTER_DELAY);
            } else {
                await delay(SCROLL_INTERVAL_MIN + Math.floor((SCROLL_INTERVAL_MAX - SCROLL_INTERVAL_MIN) * Math.random()));
            }
        }
    };

    const start = (options) => {
        const task = {
            ...options,
            running: true,
            stop() {
                this.running = false;
            },
        };

        main(task);

        return task;
    };

    let runningTask = null;

    // 添加右键菜单项
    GM_registerMenuCommand("开始挂机", () => {
        if (runningTask) {
            runningTask.stop();
        }

        runningTask = start();
    });

    GM_registerMenuCommand("停止挂机", () => {
        if (runningTask) {
            runningTask.stop();
        }
    });

    GM_registerMenuCommand("开始挂机(1小时)", () => {
        if (runningTask) {
            runningTask.stop();
        }

        runningTask = start({
            endTime: Date.now() + 1 * 60 * 60 * 1000,
        });
    });

    GM_registerMenuCommand("开始挂机(指定时长)", () => {
        const minuteStr = prompt("输入挂机时长(分钟)", "60");
        if (typeof minuteStr === 'string') {
            if (!/\d+/.test(minuteStr)) {
                alert('挂机时长输入有误,请输入数字表示的分钟数(如:60)');
                return;
            }
            const minutes = parseInt(minuteStr);
            if (runningTask) {
                runningTask.stop();
            }

            runningTask = start({
                endTime: Date.now() + minutes * 60 * 1000,
            });
        }
    });

    // 解除右键菜单限制
    document.documentElement.addEventListener('contextmenu', (e) => {
       e.stopPropagation();
    });

    // 页面固定为可见状态
    Object.defineProperty(document, 'visibilityState', {value: 'visible', writable: true});
    Object.defineProperty(document, 'hidden', {value: false, writable: true});
})();