// ==UserScript==
// @name 网页漫画下载为epub/mobi/pdf等电子书格式
// @namespace http://tampermonkey.net/
// @version 1.6.0
// @description 将网页漫画下载下来方便导入墨水屏电子书进行阅读,目前仅适用于如漫画(https://m.rumanhua.com/)
// @author MornLight
// @match https://m.rumanhua.com/*
// @match https://www.rumanhua.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org
// @grant GM_xmlhttpRequest
// @require https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js
// @run-at document-end
// @license MIT
// @supportURL https://github.com/duanmorningsir/ComicDownloader
// ==/UserScript==
(function () {
'use strict';
// 1. 样式配置
const STYLES = {
container: {
position: 'fixed',
bottom: '20px',
right: '20px',
zIndex: '1000',
display: 'flex',
flexDirection: 'column',
gap: '12px',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
padding: '15px',
borderRadius: '5px',
boxShadow: '0 0 10px rgba(0,0,0,0.1)'
},
button: {
padding: '10px',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
backgroundColor: '#4CAF50'
}
};
// 2. 站点适配器相关代码
class SiteAdapter {
constructor() {
if (this.constructor === SiteAdapter) {
throw new Error('不能直接实例化抽象类');
}
}
getChapterName() { throw new Error('必须实现 getChapterName 方法'); }
getImageElements() { throw new Error('必须实现 getImageElements 方法'); }
getImageUrl(imgElement) { throw new Error('必须实现 getImageUrl 方法'); }
}
class RumanhuaAdapter extends SiteAdapter {
getChapterName() {
const chapterNameElement = document.querySelector('.chaphead-name h1');
return chapterNameElement ? chapterNameElement.textContent.trim() : '未知章节';
}
getImageElements() {
return document.querySelectorAll('div.chapter-img-box');
}
getImageUrl(imgElement) {
const img = imgElement.querySelector('img');
return img?.src?.includes('/static/images/load.gif') ? img?.dataset?.src : img?.src;
}
}
class RumanhuaPCAdapter extends SiteAdapter {
getChapterName() {
const chapterName = document.querySelector('.headwrap .chaptername_title')?.textContent || '未知章节';
return chapterName;
}
getImageElements() {
return document.querySelectorAll('div.chapter-img-box');
}
getImageUrl(imgElement) {
const img = imgElement.querySelector('img');
return img?.src?.includes('/static/images/load.gif') ? img?.dataset?.src : img?.src;
}
}
// 3. 获取适配器的工厂函数
function getSiteAdapter() {
const url = window.location.href;
switch (true) {
case url.includes('https://www.rumanhua.com/'):
return new RumanhuaPCAdapter();
case url.includes('https://m.rumanhua.com/'):
return new RumanhuaAdapter();
default:
throw new Error('不支持的页面格式');
}
}
// 4. UI类
class DownloaderUI {
constructor(totalPages, onDownload) {
this.totalPages = totalPages;
this.onDownload = onDownload;
this.createUI();
}
createUI() {
const container = this.createContainer();
const downloadButton = this.createButton();
container.appendChild(downloadButton);
document.body.appendChild(container);
this.downloadButton = downloadButton;
}
createContainer() {
const container = document.createElement('div');
Object.assign(container.style, STYLES.container);
return container;
}
createButton() {
const downloadButton = document.createElement('button');
Object.assign(downloadButton.style, STYLES.button);
downloadButton.textContent = '下载本章节';
downloadButton.addEventListener('click', () => {
this.onDownload(1, this.totalPages);
});
return downloadButton;
}
setLoading(isLoading) {
this.downloadButton.disabled = isLoading;
this.downloadButton.textContent = isLoading ? '下载中...' : '下载本章节';
}
}
// 5. 下载器类
class ComicDownloader {
constructor() {
this.adapter = getSiteAdapter();
this.totalPages = this.adapter.getImageElements().length;
this.chapterName = this.adapter.getChapterName();
this.ui = new DownloaderUI(this.totalPages, this.handleDownload.bind(this));
}
async handleDownload() {
try {
this.ui.setLoading(true);
await this.downloadComic();
} catch (error) {
console.error('下载失败:', error);
alert('下载失败,请查看控制台了解详情');
} finally {
this.ui.setLoading(false);
}
}
async downloadComic() {
const images = await this.downloadImages(1, this.totalPages);
await this.generatePDF(images);
}
validatePageRange(start, end) {
return !(start > end || start < 1 || end > this.totalPages);
}
async downloadImages(start, end) {
const imageElements = this.adapter.getImageElements();
const downloadResults = new Array(end - start + 1);
const downloadPromises = [];
for (let i = 0; i < imageElements.length; i++) {
const element = imageElements[i];
const pageNumber = i + 1;
if (pageNumber >= start && pageNumber <= end) {
const imgUrl = this.adapter.getImageUrl(element);
if (imgUrl) {
const arrayIndex = pageNumber - start;
downloadPromises.push(
this.downloadImage(imgUrl)
.then(imgData => {
downloadResults[arrayIndex] = imgData;
})
.catch(error => {
console.error(`第 ${pageNumber} 页下载失败:`, error);
downloadResults[arrayIndex] = null;
})
);
}
}
}
await Promise.all(downloadPromises);
return downloadResults;
}
downloadImage(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
responseType: 'blob',
onload: response => this.handleImageResponse(response, resolve, reject),
onerror: error => reject(error)
});
});
}
handleImageResponse(response, resolve, reject) {
try {
const blob = response.response;
const reader = new FileReader();
reader.onload = event => resolve(event.target.result);
reader.onerror = error => reject(error);
reader.readAsDataURL(blob);
} catch (error) {
reject(error);
}
}
async generatePDF(images) {
const pdf = new jspdf.jsPDF();
images.forEach((imgData, index) => {
if (imgData) {
if (index > 0) {
pdf.addPage();
}
pdf.addImage(imgData, 'JPEG', 0, 0, pdf.internal.pageSize.getWidth(), pdf.internal.pageSize.getHeight());
}
});
pdf.save(`${this.chapterName}.pdf`);
}
}
// 6. 初始化
new ComicDownloader();
})();