嗨皮漫畫閱讀輔助

增加一些輔助閱讀功能(自用)。

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name               嗨皮漫畫閱讀輔助
// @name:zh-CN         嗨皮漫画阅读辅助
// @version            2.4.7
// @description        增加一些輔助閱讀功能(自用)。
// @description:zh-CN  增加一些辅助阅读功能(自用)。
// @author             tony0809
// @match              *://m.happymh.com/*
// @icon               https://m.happymh.com/favicon.ico
// @grant              none
// @run-at             document-end
// @license            MIT
// @namespace          https://greasyfork.org/users/20361
// ==/UserScript==

(async () => {
    'use strict';
    const options = { //true 開啟,false 關閉
            kn: true, //按鍵盤右方向鍵前往下一話。
            kp: true, //按鍵盤左方向鍵前往上一話。
            dn: true, //雙擊前往下一話,方便手機使用。
            kdn: [false, 300], //按住空白鍵超過幾毫秒後下一話。 (預設關閉)
            nE: true, //閱讀頁底部增加更新頁和收藏頁的按鈕。
            pl: true, //閱讀頁預讀全部圖片,並且嘗試預讀下一話圖片。
            hE: true, //隱藏閱讀頁頂部的公告。
            ion: [false, 1000], //下一話按鈕完全進入視窗可視範圍內時經過幾毫秒後自動下一話。 (預設關閉)
            lM: true, //更新頁自動點擊載入更多。
            list: true, //目錄頁自動展開全部章節。
            oint: true //在新分頁打開漫畫鏈接。
        },
        ge = selector => /^\//.test(selector) ? document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue : document.querySelector(selector),
        gae = selector => {
            if (/^\//.test(selector)) {
                let nodes = [];
                let results = document.evaluate(selector, document, null, XPathResult.ANY_TYPE, null);
                let node;
                while (node = results.iterateNext()) {
                    nodes.push(node);
                }
                return nodes;
            } else {
                return document.querySelectorAll(selector);
            }
        },
        lp = location.pathname,
        read = /^\/reads\/\w+\/\d+$/.test(lp),
        latest = /^\/latest$/.test(lp),
        list = /^\/manga\/\w+$/.test(lp),
        book = /^\/bookcase$/.test(lp),
        rank = /^\/rank/.test(lp),
        user = /^\/user/.test(lp),
        addGlobalStyle = css => {
            let style = document.createElement('style');
            style.type = 'text/css';
            style.innerHTML = css;
            document.head.appendChild(style);
        },
        hasTouchEvents = () => {
            if (('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)) {
                return true;
            }
            return false;
        },
        loadMore = selector => {
            let loadMoreButton = ge(selector);
            if (hasTouchEvents()) {
                let dispatchTouchEvent = (ele, type) => {
                    let touchEvent = document.createEvent('UIEvent');
                    touchEvent.initUIEvent(type, true, true);
                    touchEvent.touches = [{
                        clientX: 1,
                        clientY: 1
                    }];
                    ele.dispatchEvent(touchEvent);
                };
                dispatchTouchEvent(loadMoreButton, "touchstart");
                dispatchTouchEvent(loadMoreButton, "touchend");
                console.log('嗨皮漫畫模擬觸控點擊');
                //loadMoreButton.dispatchEvent(new Event("touchstart"));
                //loadMoreButton.dispatchEvent(new Event("touchend"));
            } else {
                loadMoreButton.click();
                console.log('嗨皮漫畫模擬點擊');
            }
        },
        openInNewTab = () => gae('.home-banner a:not([target=_blank]),.manga-rank a:not([target=_blank]),.manga-cover a:not([target=_blank])').forEach(a => {
            a.setAttribute('target', '_blank');
        }),
        waitEle = selector => {
            return new Promise(resolve => {
                let loop = setInterval(() => {
                    if (!!ge(selector) === true) {
                        clearInterval(loop);
                        resolve();
                    }
                }, 100);
            });
        },
        preLoad = (pn, text) => {
            let lps = pn.split('/'),
                mangaCode = lps[2],
                id = lps[3],
                apiUrl = `/v2.0/apis/manga/read?code=${mangaCode}&cid=${id}&v=v2.13`;
            fetch(apiUrl).then(res => res.json()).then(async jsonData => {
                try {
                    if (jsonData.status == 0) {
                        console.log(text + '漫畫名稱:' + jsonData.data.manga_name + '\n章節名稱:' + jsonData.data.chapter_name + '\n章節圖片:\n', jsonData.data.scans, '\nJSON:\n', jsonData);
                        let imgs = jsonData.data.scans;
                        for (let i = 0; i < imgs.length; i++) {
                            let img = new Image();
                            img.src = imgs[i].url;
                            img.referrerPolicy = "origin";
                            await new Promise(resolve => {
                                setTimeout(resolve, 200);
                            });
                        }
                    } else if (jsonData.status == 403) {
                        console.log(text + '獲取數據失敗\n', jsonData);
                    }
                } catch (error) {
                    console.error(error);
                }
            }).catch(error => console.error(error));
        };

    if (ge(".no-js")) return; //Cloudflare 檢測連線安全性時,不執行後續的代碼。

    if (options.oint && !read && !list && !user) {
        openInNewTab();
        console.log('嗨皮漫畫在新分頁打開漫畫鏈接');
        new MutationObserver(() => {
            openInNewTab();
        }).observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    if (options.lM && latest) {
        new IntersectionObserver(entries => {
            if (entries[0].isIntersecting) {
                loadMore('.more-div-btn');
                console.log('載入更多');
            }
        }).observe(ge('.more-div-btn'));
    }

    if (options.list && list) {
        setTimeout(() => {
            ge('#expandButton').click();
            console.log('嗨皮漫畫自動展開目錄');
        }, 1000);
    }

    if (options.hE && read) {
        addGlobalStyle('#root>div>header+div{display:none!important}');
    }

    if (options.kn && read) {
        document.addEventListener('keydown', (e) => {
            let key = window.event ? e.keyCode : e.which;
            if (key == 39) {
                let n = ge("//a[span[text()='下一话' or text()='下一話'] and contains(@href,'reads')]");
                if (n) {
                    location.href = n.href;
                } else {
                    alert('沒有下一话了!');
                }
            }
        });
    }

    if (options.kp && read) {
        document.addEventListener('keydown', (e) => {
            let key = window.event ? e.keyCode : e.which;
            if (key == 37) {
                let p = ge("//a[span[text()='上一话' or text()='上一話'] and contains(@href,'reads')]");
                if (p) {
                    location.href = p.href;
                } else {
                    alert('沒有上一话了!');
                }
            }
        });
    }

    if (options.dn && read) {
        document.addEventListener('dblclick', () => {
            let n = ge('footer a');
            location.href = n.href;
        });
    }

    if (options.kdn[0] && read) {
        let timeId;
        document.addEventListener('keypress', (e) => {
            let key = window.event ? e.keyCode : e.which;
            if (key == 32) {
                let n = ge("//a[span[text()='下一话' or text()='下一話'] and contains(@href,'reads')]");
                if (n) {
                    timeId = setTimeout(() => {
                        location.href = n.href;
                    }, options.kdn[1]);
                } else {
                    timeId = setTimeout(() => {
                        alert('沒有下一話了!');
                    }, options.kdn[1]);
                }
            }
        });
        document.addEventListener('keyup', (e) => {
            let key = window.event ? e.keyCode : e.which;
            if (key == 32) {
                clearTimeout(timeId);
            }
        });
    }

    if (options.pl && read) {
        await waitEle('[id^=imageLoader]');
        console.log('嗨皮漫畫預讀全部圖片');
        preLoad(lp, '嗨皮漫畫本話數據\n');
        setTimeout(() => {
            let next = ge("//span[@id and text()='下一话' or text()='下一話']/following-sibling::a[1][contains(@href,'reads')]");
            if (next) {
                preLoad(next.pathname, '嗨皮漫畫下一話數據\n');
            }
        }, 3000);
    }

    if (options.nE && read) {
        await waitEle('#page-area');
        addGlobalStyle('footer>article>div{padding: 0.5rem 0 !important}');
        new IntersectionObserver((entries, observer) => {
            if (entries[0].isIntersecting) {
                observer.unobserve(entries[0].target);
                let f = ge('footer>article');
                let item = ge("footer>article>div:nth-child(2)");
                item.querySelectorAll('a').forEach(a => a.classList.add('MuiButton-containedPrimary'));
                let p = ge("//a[span[text()='上一话' or text()='上一話'] and contains(@href,'reads')]");
                if (p) {
                    p.classList.add('MuiButton-containedPrimary');
                }
                let n = ge("//a[span[text()='下一话' or text()='下一話'] and contains(@href,'readMore')]");
                if (n) {
                    n.classList.remove('MuiButton-containedPrimary');
                    n.firstChild.innerText = '^_^感谢您的阅读~已经没有下一话了哦~';
                }
            }
        }).observe(ge('#page-area'));
    }

    if (options.ion[0] && read) {
        await waitEle("//a[span[text()='下一话' or text()='下一話']]");
        let timeId;
        new IntersectionObserver(entries => {
            if (entries[0].isIntersecting) {
                timeId = setTimeout(() => {
                    let n = ge("//a[span[text()='下一话' or text()='下一話'] and contains(@href,'reads')]");
                    if (n) location.href = n.href;
                }, options.ion[1]);
            } else {
                clearTimeout(timeId);
            }
        }, {
            threshold: 1,
        }).observe(ge('footer a'));
    }

})();