Yamibo Manga to PDF

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

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

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.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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 + '%';
        }
    }
})();