NovelJump

轻松跳转小说章节

// ==UserScript==
// @name         NovelJump
// @name:zh      小说章节跳转
// @namespace    https://github.com/SuniRein/scripts
// @version      1.1.0
// @description  轻松跳转小说章节
// @author       SuniRein
// @match        https://www.linovelib.com/*
// @grant        none
// @icon         https://www.google.com/s2/favicons?sz=64&domain=linovelib.com
// @license      GPL3
// @supportURL   https://github.com/SuniRein/scripts/blob/main/CHANGELOG.md
// ==/UserScript==

(function () {
    'use strict';

    function getCurrentUrlInfo() {
        const match = location.pathname.match(/\/novel\/(\d+)\/(\d+)(?:_(\d+))?\.html/);
        if (!match) {
            console.error('[NovelJump] 当前页面 URL 格式不符合预期');
            return null;
        }
        return {
            novelId: parseInt(match[1], 10),
            chapterId: parseInt(match[2], 10),
            sectionId: match[3] ? parseInt(match[3], 10) : 1,
        };
    }

    function generateUrl(novelId, chapterId, sectionId) {
        return `/novel/${novelId}/${chapterId}${sectionId > 1 ? '_' + sectionId : ''}.html`;
    }

    function jump(novelId, chapterId, sectionId) {
        if (chapterId < 1) {
            console.warn('[NovelJump] 无效的章节编号: ', chapterId);
            return;
        }

        if (sectionId < 1) {
            console.warn('[NovelJump] 无效的节偏移值');
            return;
        }

        window.location.href = generateUrl(novelId, chapterId, sectionId);
    }

    function getFinalSectionId(info) {
        if (info.sectionId == 1) {
            console.log('[NovelJump] 当前章节为第一节,无法获取最后一节 ID');
            return null;
        }

        const title = document.getElementsByTagName('h1')[0].textContent;
        const finalSectionIdStr = title.match(/((\d+)\/(\d+))/)[2];
        const finalSectionId = parseInt(finalSectionIdStr, 10);
        console.log('[NovelJump] 成功获取最后一节 ID');

        return finalSectionId;
    }

    // 创建悬浮框并添加到页面
    function createFloatingPanel(info) {
        const { novelId, chapterId, sectionId } = info;

        const panel = document.createElement('div');
        panel.id = 'chapter-nav-box';
        panel.style.position = 'fixed';
        panel.style.top = '50%';
        panel.style.right = '10px';
        panel.style.transform = 'translateY(-50%)';
        panel.style.backgroundColor = 'rgba(255, 255, 255, 0.95)';
        panel.style.border = '1px solid #ccc';
        panel.style.borderRadius = '8px';
        panel.style.padding = '10px';
        panel.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.15)';
        panel.style.zIndex = '9999';
        panel.style.fontFamily = 'sans-serif';
        panel.style.fontSize = '14px';
        panel.style.width = '90px';

        const prevBtn = document.createElement('button');
        prevBtn.textContent = '上一章';
        prevBtn.onclick = () => jump(novelId, chapterId - 1, 1);

        const nextBtn = document.createElement('button');
        nextBtn.textContent = '下一章';
        nextBtn.onclick = () => jump(novelId, chapterId + 1, 1);

        const input = document.createElement('input');
        input.type = 'number';
        input.min = '1';
        input.placeholder = '节号';
        input.id = 'section-input';
        input.style.width = '100%';
        input.style.marginTop = '6px';
        input.style.boxSizing = 'border-box';
        input.value = sectionId + 1; // 自动填充下一节

        const goBtn = document.createElement('button');
        goBtn.textContent = '跳转节';
        goBtn.onclick = () => {
            const sectionInput = document.getElementById('section-input');
            const section = parseInt(sectionInput.value, 10);
            if (isNaN(section) || section <= 0) {
                alert('请输入有效的节号(正整数)!');
                return;
            }
            jump(novelId, chapterId, section);
        };

        let elements = [prevBtn, nextBtn, input, goBtn];

        let finalSectionId = getFinalSectionId(info);
        if (finalSectionId) {
            const lastBtn = document.createElement('button');
            lastBtn.textContent = '最后一节';
            lastBtn.onclick = () => jump(novelId, chapterId, finalSectionId);
            elements.push(lastBtn);
        }

        elements.forEach((el) => {
            el.style.display = 'block';
            el.style.margin = '4px 0';
            el.style.width = '100%';
            el.style.padding = '6px';
            if (el.tagName === 'BUTTON') {
                el.style.backgroundColor = '#409eff';
                el.style.color = '#fff';
                el.style.border = 'none';
                el.style.borderRadius = '4px';
                el.style.cursor = 'pointer';
                el.onmouseenter = () => (el.style.backgroundColor = '#66b1ff');
                el.onmouseleave = () => (el.style.backgroundColor = '#409eff');
            }
            panel.appendChild(el);
        });

        document.body.appendChild(panel);
    }

    console.log('[NovelJump] 解析页面信息');
    const info = getCurrentUrlInfo();
    console.debug('[NovelJump] 当前页面信息:', info);

    function waitForBodyAndInit() {
        const checkInterval = setInterval(() => {
            if (document.body) {
                clearInterval(checkInterval);
                createFloatingPanel(info);
                console.log('[NovelJump] 脚本加载完成');
            }
        }, 100);
    }

    waitForBodyAndInit();
})();