Greasy Fork

Greasy Fork is available in English.

Arrow Keys: Next/Prev Chapter

Use arrow keys (← / →) to navigate next/previous chapters on supported sites

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Arrow Keys: Next/Prev Chapter
// @namespace    https://github.com/Astropilot-Fizzfaldt-Merge
// @version      1.2.0
// @description  Use arrow keys (← / →) to navigate next/previous chapters on supported sites
// @author       Astropilot + Fizzfaldt + ChatGPT
// @license      MIT
// @run-at       document-end
// @grant        none
// @noframes
// @match        https://comix.to/*
// @match        *://*.archiveofourown.org/*/chapters/*
// @match        *://*.asuracomic.net/*
// @match        *://*.asuratoon.com/*
// @match        *://*.fanfiction.net/*
// @match        *://*.flamescans.com/*chapter*
// @match        *://*.manga-scans.com/chapter/*
// @match        *://*.oglaf.com/*
// @match        *://*.realmscans.xyz/*-chapter-*
// @match        *://*.reaperscans.com/*
// @match        *://*.strongfemaleprotagonist.com/*
// @match        *://*.tthfanfic.org/*
// @match        *://*.webtoons.com/*/viewer*episode_no=*
// @match        *://*.wuxiaworld.co/*
// @match        *://*.wuxiaworld.com/*
// @match        *://*.xcalibrscans.com/*-chapter-*
// @match        *://*.mangakakalot.com/chapter/*
// @match        *://*.chapmanganato.to/manga-*/chapter-*
// @match        *://*.scyllacomics.xyz/manga/*/*
// @match        *://*.mangagalaxy.org/series/*/chapter-*
// @match        *://*.natomanga.com/manga/*/chapter-*
// @match        *://*.colamanga.com/manga-*/*/*.html
// @match        *://readcomiconline.li/Comic/*
// @match        *://*.vortexscans.org/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/psl.min.js
// @icon         https://cdn-icons-png.flaticon.com/512/1782/1782564.png
// ==/UserScript==

(function () {
    "use strict";

    /* ------------------------------
     * XPATH MAP (unchanged except Mangafire removed)
     * ------------------------------ */
    const xpathMap = {
        "archiveofourown.org": {
            prev: '//a[@href and text()="← Previous Chapter"]/@href',
            next: '//a[@href and text()="Next Chapter →"]/@href',
        },
        "xcalibrscans.com": {
            prev: '//a[@class="ch-prev-btn" and @href]/@href',
            next: '//a[@class="ch-next-btn" and @href]/@href',
        },
    };

    /* ------------------------------
     * SELECTOR MAP
     * Added: vortexscans.org
     * Removed: mangafire.to
     * ------------------------------ */
    const selectorMap = [
        { hosts: ["manga-scans.com"], selectors: { prev: "div.prev-post > a", next: "div.next-post > a" } },
        { hosts: ["reaperscans.com"], selectors: { prev: "main nav div.flex:nth-of-type(1) > a", next: "main nav div.flex:nth-of-type(3) > a:nth-of-type(2)" } },
        { hosts: ["webtoons.com"], selectors: { prev: "a._prevEpisode", next: "a._nextEpisode" } },
        { hosts: ["mangakakalot.com"], selectors: { prev: ".btn-navigation-chap > a.next", next: ".btn-navigation-chap > a.back" } },
        { hosts: ["chapmanganato.to"], selectors: { prev: ".navi-change-chapter-btn > a.navi-change-chapter-btn-prev", next: ".navi-change-chapter-btn > a.navi-change-chapter-btn-next" } },
        { hosts: ["scyllacomics.xyz"], selectors: { prev: "main section div.relative div.flex div.grid a:nth-of-type(1)", next: "main section div.relative div.flex div.grid a:nth-of-type(2)" } },
        { hosts: ["mangagalaxy.org"], selectors: { prev: "a[aria-label='Prev']", next: "a[aria-label='Next']" } },
        { hosts: ["natomanga.com"], selectors: { prev: ".btn-navigation-chap a.next", next: ".btn-navigation-chap a.back" } },
        { hosts: ["colamanga.com"], selectors: { prev: ".mh_headpager a.read_page_link:first-of-type", next: ".mh_headpager a.read_page_link:last-of-type" } },
        { hosts: ["readcomiconline.li"], selectors: { prev: 'img[title="Previous Issue"]', next: 'img[title="Next Issue"]' }, getLinkFromImage: true },
        { hosts: ["asuracomic.net"], selectors: { prev: "a.prev", next: "a.next" } },

        /* ★★★ NEW — vortexscans.org ★★★ */
        { hosts: ["vortexscans.org"], selectors: {
            prev: 'button[aria-label="Prev"]',
            next: 'button[aria-label="Next"]'
        }},
    ];

    /* helpers */
    const currentDomain = psl.get(window.location.hostname);
    const selectorRule = selectorMap.find(r => r.hosts.includes(currentDomain));

    function safeNavigate(href) {
        if (!href) return;
        try {
            const resolved = new URL(href, window.location.href).href;
            window.location.href = resolved;
        } catch (err) {}
    }

    function getXPathLink(expr) {
        try {
            const res = document.evaluate(expr, document, null, XPathResult.ANY_TYPE, null);
            const node = res.iterateNext();
            return node ? node.value : null;
        } catch (e) { return null; }
    }

    function genericFindLinkBySelectorRule(direction) {
        if (!selectorRule) return null;
        const sel = selectorRule.selectors[direction];
        if (!sel) return null;
        try { return document.querySelector(sel); } catch (e) { return null; }
    }

    /* ------------------------------
     * Main Key Listener
     * ------------------------------ */
    document.addEventListener("keydown", (e) => {
        if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target.tagName) || e.target.isContentEditable) return;
        if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
        if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;

        const dir = e.key === "ArrowLeft" ? "prev" : "next";

        // 1) XPath rule
        const xpathRule = xpathMap[currentDomain];
        if (xpathRule && xpathRule[dir]) {
            const link = getXPathLink(xpathRule[dir]);
            if (link) {
                safeNavigate(link);
                e.preventDefault();
                return;
            }
        }

        // 2) Selector rules
        const selEl = genericFindLinkBySelectorRule(dir);
        if (selEl) {
            if (selEl.tagName === "A" && selEl.href) {
                safeNavigate(selEl.href);
            } else {
                selEl.click(); // works for VortexScans buttons
            }
            e.preventDefault();
            return;
        }

        // 3) Fallback text-based search
        const anchors = Array.from(document.querySelectorAll("a[href]"));
        for (const a of anchors) {
            const txt = a.textContent.toLowerCase();
            if (dir === "prev" && (txt.includes("prev") || txt.includes("previous"))) {
                safeNavigate(a.href);
                e.preventDefault();
                return;
            }
            if (dir === "next" && txt.includes("next")) {
                safeNavigate(a.href);
                e.preventDefault();
                return;
            }
        }
    });
})();


/* =========================================================
   APPENDED — COMIX.TO SCRIPT (UNCHANGED)
   ========================================================= */

(function () {
    'use strict';

    const SCROLL_AMOUNT = 80;

    function getScrollableElement() {
        const elements = document.querySelectorAll('*');
        let best = null;
        let maxScroll = 0;

        for (const el of elements) {
            const scrollHeight = el.scrollHeight - el.clientHeight;
            if (scrollHeight > maxScroll && scrollHeight > 200) {
                maxScroll = scrollHeight;
                best = el;
            }
        }

        return best || document.documentElement;
    }

    window.addEventListener(
        'keydown',
        function (e) {
            const el = document.activeElement;
            if (
                el &&
                (el.tagName === 'INPUT' ||
                    el.tagName === 'TEXTAREA' ||
                    el.isContentEditable)
            ) return;

            if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;

            const scrollEl = getScrollableElement();
            if (!scrollEl) return;

            e.stopImmediatePropagation();
            e.preventDefault();

            const delta = e.key === 'ArrowDown' ? SCROLL_AMOUNT : -SCROLL_AMOUNT;
            scrollEl.scrollTop += delta;
        },
        true
    );
})();