Yamibo Manga to PDF

使用最后两个<li>内容命名PDF,自动抓取图片并生成PDF

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         Yamibo Manga to PDF
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  使用最后两个<li>内容命名PDF,自动抓取图片并生成PDF
// @author       You
// @match        https://www.yamibo.com/*
// @grant        GM_xmlhttpRequest
// @connect      img.yamibo.com
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jspdf.umd.min.js
// ==/UserScript==

(function() {
    'use strict';

    createFloatingButton();

    function createFloatingButton() {
        const button = document.createElement('button');
        button.textContent = '📥 下载为PDF';
        Object.assign(button.style, {
            position: 'fixed',
            right: '20px',
            bottom: '20px',
            zIndex: '99999',
            backgroundColor: '#2196F3',
            color: 'white',
            border: 'none',
            padding: '12px 24px',
            borderRadius: '8px',
            fontSize: '16px',
            cursor: 'pointer',
            boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
            transition: 'all 0.3s ease'
        });

        button.addEventListener('mouseover', () => {
            button.style.backgroundColor = '#1976D2';
        });
        button.addEventListener('mouseout', () => {
            button.style.backgroundColor = '#2196F3';
        });

        button.onclick = () => {
            console.log('开始收集图片...');
            startCrawling();
        };

        document.body.appendChild(button);
    }

    function startCrawling() {
        const breadcrumbList = document.querySelectorAll('ul.breadcrumb > li');

        if (breadcrumbList.length < 2) {
            showNotification('❌ 未找到足够的面包屑导航项', 'error');
            return;
        }

        const mangaTitle = breadcrumbList[breadcrumbList.length - 2].innerText.trim();
        const chapterTitle = breadcrumbList[breadcrumbList.length - 1].innerText.trim();

        const filename = `${sanitizeFilename(mangaTitle)}-${sanitizeFilename(chapterTitle)}.pdf`;

        showNotification(`📄 文件将保存为:${filename}`);

        const imageUrls = [];
        const imgElement = document.getElementById('imgPic');

        if (!imgElement) {
            showNotification('❌ 未找到图片元素', 'error');
            return;
        }

        imageUrls.push(imgElement.src);
        showNotification(`✅ 已收集第1页图片`);

        let nextPageUrl = document.querySelector('li.next a')?.href;
        let pageCount = 1;

        if (!nextPageUrl) {
            showNotification('🔚 仅发现单页内容,即将生成PDF');
            generatePDF(imageUrls, filename);
            return;
        }

        const processPages = () => {
            if (!nextPageUrl) {
                showNotification(`✅ 完成!共收集到${pageCount}页图片`);
                generatePDF(imageUrls, filename);
                return;
            }

            GM_xmlhttpRequest({
                method: 'GET',
                url: nextPageUrl,
                headers: {
                    'Referer': location.origin
                },
                onload: function(response) {
                    if (response.status === 200) {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(response.responseText, 'text/html');

                        const img = doc.getElementById('imgPic');
                        if (img) {
                            pageCount++;
                            imageUrls.push(img.src);
                            showNotification(`✅ 已收集第${pageCount}页图片`);
                        }

                        const nextLink = doc.querySelector('li.next a');
                        nextPageUrl = nextLink ? new URL(nextLink.href, location.origin).href : null;

                        processPages();
                    }
                },
                onerror: function(err) {
                    showNotification(`网络错误: ${err.statusText}`, 'error');
                }
            });
        };

        processPages();
    }

    function generatePDF(imageUrls, filename) {
        const loading = showLoadingNotification('⏳ 正在生成PDF...');
        const pdf = new jspdf.jsPDF('p', 'pt', 'a4');
        const pageWidth = pdf.internal.pageSize.getWidth();
        const pageHeight = pdf.internal.pageSize.getHeight();
        let loadedCount = 0;

        const downloadImage = (index) => {
            if (index >= imageUrls.length) {
                loading.remove();
                pdf.save(filename);
                showNotification('✅ PDF生成完成!');
                return;
            }

            GM_xmlhttpRequest({
                method: 'GET',
                url: imageUrls[index],
                responseType: 'blob',
                headers: {
                    'Referer': 'https://www.yamibo.com/'
                },
                onload: function(response) {
                    const reader = new FileReader();
                    reader.onload = function() {
                        const imgData = reader.result;
                        if (index > 0) pdf.addPage();
                        pdf.addImage(imgData, 'JPG', 0, 0, pageWidth, pageHeight);
                        loadedCount++;
                        updateProgress(loadedCount, imageUrls.length);
                        downloadImage(index + 1);
                    };
                    reader.readAsDataURL(response.response);
                }
            });
        };

        downloadImage(0);
    }

    function sanitizeFilename(str) {
        const illegalRe = /[\\\/\:\*\?"<>\|]/g;
        const fullWidthRe = /[\uFF01-\uFF5E]/g;
        return str.replace(illegalRe, '')
                 .replace(fullWidthRe, c => String.fromCharCode(c.charCodeAt(0) - 0xfee0))
                 .trim();
    }

    function showNotification(message, type = 'info') {
        const colors = { info: '#2196F3', success: '#4CAF50', error: '#F44336' };
        const notification = document.createElement('div');
        notification.style.cssText = `
            position: fixed;
            right: 20px;
            bottom: 80px;
            background: ${colors[type]};
            color: white;
            padding: 12px 24px;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.2);
            z-index: 99998;
            animation: slideIn 0.3s ease-out forwards;
            opacity: 0;
            transform: translateY(20px);
        `;
        notification.innerHTML = `
            <style>
                @keyframes slideIn {
                    to { opacity: 1; transform: translateY(0); }
                }
            </style>
            ${message}
        `;
        document.body.appendChild(notification);
        setTimeout(() => {
            notification.style.animation = 'slideOut 0.3s ease-in forwards';
            notification.addEventListener('animationend', () => notification.remove());
        }, 3000);
        return notification;
    }

    function showLoadingNotification(message) {
        const container = document.createElement('div');
        container.style.cssText = `
            position: fixed;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            background: #ffffffee;
            padding: 16px 24px;
            border-radius: 8px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.15);
            z-index: 99999;
            font-weight: bold;
            min-width: 200px;
            text-align: center;
        `;
        container.innerHTML = `
            <div style="margin-bottom: 8px"><span class="spinner"></span></div>
            ${message}
            <div id="progress-bar" style="width: 100%; height: 6px; background: #eee; margin-top: 12px; border-radius: 3px; overflow: hidden;">
                <div id="progress-fill" style="width: 0%; height: 100%; background: #2196F3; transition: width 0.3s;"></div>
            </div>
            <style>
                .spinner {
                    display: inline-block;
                    width: 24px;
                    height: 24px;
                    border: 3px solid #2196F3;
                    border-radius: 50%;
                    border-top-color: transparent;
                    animation: spin 1s infinite linear;
                }
                @keyframes spin {
                    to { transform: rotate(360deg); }
                }
            </style>
        `;
        document.body.appendChild(container);
        return container;
    }

    function updateProgress(current, total) {
        const progressFill = document.getElementById('progress-fill');
        if (progressFill) {
            const percent = Math.round((current / total) * 100);
            progressFill.style.width = percent + '%';
        }
    }
})();