// ==UserScript== // @name 网页漫画下载为pdf格式 // @namespace http://tampermonkey.net/ // @version 2.1.0 // @description 将网页漫画下载为pdf方便阅读,目前仅适用于如漫画(电脑版)[https://m.rumanhua.com/]、(手机版)[https://www.rumanhua.com/] // @author MornLight // @match https://m.rumanhua.com/* // @match http://www.rumanhua1.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org // @grant GM_xmlhttpRequest // @grant GM_openInTab // @grant GM_setValue // @grant GM_getValue // @connect * // @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 // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 1. 样式配置 const STYLES = { container: { position: 'fixed', bottom: '20px', right: '20px', zIndex: '9999', // 提高 z-index 确保在最上层 display: 'flex', flexDirection: 'column', gap: '12px', backgroundColor: 'rgba(255, 255, 255, 0.95)', // 增加不透明度 padding: '15px', borderRadius: '5px', boxShadow: '0 0 10px rgba(0,0,0,0.2)', // 增强阴影 maxHeight: '80vh', overflowY: 'auto', minWidth: '250px' // 确保最小宽度 }, button: { padding: '10px', color: '#fff', border: 'none', borderRadius: '5px', cursor: 'pointer', backgroundColor: '#4CAF50', transition: 'background-color 0.3s' }, cancelButton: { backgroundColor: '#f44336', display: 'none' }, progressContainer: { display: 'none' }, infoText: { color: '#666', fontSize: '14px', textAlign: 'center', marginBottom: '8px' } }; // 2. 站点适配器相关代码 class SiteAdapter { isChapterPage() { throw new Error('必须实现 isChapterPage 方法'); } isDirectoryPage() { throw new Error('必须实现 isDirectoryPage 方法'); } getChapterLinks() { throw new Error('必须实现 getChapterLinks 方法'); } getChapterName() { throw new Error('必须实现 getChapterName 方法'); } getImageElements() { throw new Error('必须实现 getImageElements 方法'); } getImageUrl(imgElement) { throw new Error('必须实现 getImageUrl 方法'); } } class RumanhuaAdapter extends SiteAdapter { isChapterPage() { const url = window.location.href; // 恢复原来的匹配模式 const chapterPagePattern = /https:\/\/m\.rumanhua\.com\/[^\/]+\/[^\/]+\.html/; return chapterPagePattern.test(url); } isDirectoryPage() { const url = window.location.href; // 恢复原来的匹配模式 const directoryPagePattern = /https:\/\/m\.rumanhua\.com\/[^\/]+\/?$/; return directoryPagePattern.test(url); } getChapterLinks() { const chapterListElement = document.querySelector('.chapterlistload ul'); if (!chapterListElement) { throw new Error('未找到章节列表'); } const chapterElements = chapterListElement.querySelectorAll('a'); return Array.from(chapterElements).map(element => element.href); } 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 { isChapterPage() { const url = window.location.href; // 恢复原来的匹配模式 const chapterPagePattern = /http:\/\/www\.rumanhua1\.com\/[^\/]+\/[^\/]+\.html/; return chapterPagePattern.test(url); } isDirectoryPage() { const url = window.location.href; // 恢复原来的匹配模式 const directoryPagePattern = /http:\/\/www\.rumanhua1\.com\/[^\/]+\/?$/; return directoryPagePattern.test(url); } getChapterLinks() { const chapterListElement = document.querySelector('.chapterlistload ul'); if (!chapterListElement) { throw new Error('未找到章节列表'); } const chapterElements = chapterListElement.querySelectorAll('a'); return Array.from(chapterElements).map(element => element.href); } 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('http://www.rumanhua1.com/'): return new RumanhuaPCAdapter(); case url.includes('https://m.rumanhua.com/'): return new RumanhuaAdapter(); default: throw new Error('不支持的页面格式'); } } // 4. UI类 // 4.1 普通下载器UI class DownloaderUI { constructor(totalPages, onDownload, onCancel) { this.totalPages = totalPages; this.onDownload = onDownload; this.onCancel = onCancel; this.currentPage = 0; this.createUI(); } // 添加 createElement 方法 createElement(type, styles, textContent = '') { const element = document.createElement(type); if (type === 'input' && styles.type) { element.type = styles.type; delete styles.type; } if (typeof styles === 'string') { element.className = styles; } else { Object.assign(element.style, styles); } if (textContent) element.textContent = textContent; return element; } createUI() { const container = this.createContainer(); // 添加页数信息 this.infoText = this.createElement('div', STYLES.infoText, `本章节共 ${this.totalPages} 页`); container.appendChild(this.infoText); // 添加长图模式切换按钮 this.longPageModeButton = this.createElement('button', { ...STYLES.button, backgroundColor: '#2196F3', marginBottom: '10px' }, '切换长图模式'); this.longPageModeButton.addEventListener('click', () => this.toggleLongPageMode()); container.appendChild(this.longPageModeButton); this.downloadButton = this.createButton('下载本章节', () => this.onDownload(1, this.totalPages)); this.cancelButton = this.createButton('取消下载', () => { this.onCancel(); this.infoText.textContent = '下载已取消'; setTimeout(() => { this.infoText.textContent = `本章节共 ${this.totalPages} 页`; }, 2000); }, true); // 创建进度容器 this.progressContainer = this.createElement('div', STYLES.progressContainer); this.progressBar = this.createProgressBar(); this.progressText = this.createElement('span', { marginLeft: '10px' }); this.progressContainer.appendChild(this.progressBar); this.progressContainer.appendChild(this.progressText); container.appendChild(this.downloadButton); container.appendChild(this.cancelButton); container.appendChild(this.progressContainer); document.body.appendChild(container); } setLoading(isLoading, showCancel = false) { this.downloadButton.disabled = isLoading; this.downloadButton.style.backgroundColor = isLoading ? '#999' : '#4CAF50'; this.downloadButton.style.cursor = isLoading ? 'not-allowed' : 'pointer'; this.downloadButton.textContent = isLoading ? '下载中...' : '下载本章节'; this.downloadButton.style.display = isLoading ? 'none' : 'block'; // 修改:控制下载按钮显示 this.cancelButton.style.display = showCancel ? 'block' : 'none'; this.progressContainer.style.display = isLoading ? 'block' : 'none'; this.infoText.style.display = isLoading ? 'none' : 'block'; } updateProgress(currentPage) { this.currentPage = currentPage; this.progressBar.value = currentPage; const percent = ((currentPage / this.totalPages) * 100).toFixed(2); this.progressText.textContent = `${currentPage}/${this.totalPages} (${percent}%)`; } createButton(text, onClick, isCancel = false) { const button = document.createElement('button'); Object.assign(button.style, STYLES.button); // 应用基础按钮样式 if (isCancel) { Object.assign(button.style, STYLES.cancelButton); // 如果是取消按钮,额外应用取消按钮样式 } button.textContent = text; button.addEventListener('click', onClick); return button; } createContainer() { const container = document.createElement('div'); Object.assign(container.style, STYLES.container); return container; } createProgressBar() { const progressBar = document.createElement('progress'); progressBar.max = this.totalPages; progressBar.value = 0; progressBar.style.width = '100%'; return progressBar; } toggleLongPageMode() { this.isLongPageMode = !this.isLongPageMode; this.longPageModeButton.textContent = this.isLongPageMode ? '切换普通模式' : '切换长图模式'; this.longPageModeButton.style.backgroundColor = this.isLongPageMode ? '#4CAF50' : '#2196F3'; } } // 4.2 章节选择器UI class ChapterSelectorUI { constructor(onDownloadSelected) { this.onDownloadSelected = onDownloadSelected; this.selectedChapters = new Set(); this.isSelectionMode = false; this.createUI(); // 移除这行,不在构造函数中初始化章节列表 // this.initChapterList(); } // 添加 createElement 方法 createElement(type, styles, textContent = '') { const element = document.createElement(type); if (type === 'input' && styles.type) { element.type = styles.type; delete styles.type; } if (typeof styles === 'string') { element.className = styles; } else { Object.assign(element.style, styles); } if (textContent) element.textContent = textContent; return element; } createUI() { // 创建容器 this.container = this.createElement('div', STYLES.container); document.body.appendChild(this.container); // 创建【选择章节下载】按钮 - 修改样式确保可见 this.selectButton = this.createElement('button', { padding: '10px', color: '#fff', border: 'none', borderRadius: '5px', cursor: 'pointer', backgroundColor: '#4CAF50', transition: 'background-color 0.3s', position: 'sticky', top: '0', zIndex: '10', width: '100%', marginBottom: '10px', fontWeight: 'bold' }, '选择章节下载'); this.selectButton.addEventListener('click', () => this.toggleSelectionMode()); this.container.appendChild(this.selectButton); // 添加取消按钮 - 初始状态为隐藏 this.cancelSelectionButton = this.createElement('button', { padding: '8px', color: '#fff', border: 'none', borderRadius: '5px', cursor: 'pointer', backgroundColor: '#f44336', transition: 'background-color 0.3s', position: 'sticky', top: '40px', zIndex: '10', width: '100%', marginBottom: '10px', display: 'none' }, '返回'); // 修改文本为"返回" this.cancelSelectionButton.addEventListener('click', () => this.cancelSelectionMode()); this.container.appendChild(this.cancelSelectionButton); // 创建章节列表容器 - 添加滚动条 this.chapterListContainer = this.createElement('div', { marginTop: '10px', display: 'none', maxHeight: '50vh', overflowY: 'auto', paddingRight: '5px' }); this.container.appendChild(this.chapterListContainer); // 添加进度显示区域 - 固定在底部 this.progressContainer = this.createElement('div', { marginTop: '10px', display: 'none', position: 'sticky', bottom: '0', backgroundColor: 'rgba(255, 255, 255, 0.9)', padding: '5px 0', zIndex: '2' }); this.progressText = this.createElement('div', { marginBottom: '5px', fontSize: '14px', color: '#666' }); this.progressBar = document.createElement('progress'); this.progressBar.style.width = '100%'; this.progressContainer.appendChild(this.progressText); this.progressContainer.appendChild(this.progressBar); this.container.appendChild(this.progressContainer); } initChapterList() { // 清空现有章节列表 this.chapterListContainer.innerHTML = ''; this.selectedChapters.clear(); const chapterListElement = document.querySelector('.chapterlistload ul'); if (!chapterListElement) { console.error('未找到章节列表'); return; } const chapterElements = chapterListElement.querySelectorAll('a'); console.log(`找到 ${chapterElements.length} 个章节`); // 添加简单的控制区 const controlsContainer = this.createElement('div', { display: 'flex', justifyContent: 'space-between', // 修改为两端对齐 marginBottom: '10px', position: 'sticky', top: '0', backgroundColor: 'rgba(255, 255, 255, 0.9)', padding: '5px 0', zIndex: '1' }); // 添加章节数量显示 const chapterCountLabel = this.createElement('span', { fontSize: '12px', color: '#666', alignSelf: 'center' }, `共 ${chapterElements.length} 章`); // 创建按钮容器,用于放置多个按钮 const buttonsContainer = this.createElement('div', { display: 'flex', gap: '5px' }); // 添加刷新按钮 const refreshBtn = this.createElement('button', { padding: '3px 8px', fontSize: '12px', backgroundColor: '#2196F3', color: 'white', border: 'none', borderRadius: '3px', cursor: 'pointer' }, '刷新列表'); refreshBtn.addEventListener('click', () => this.refreshChapterList()); // 修改为"清除选择"按钮 const deselectAllBtn = this.createElement('button', { padding: '3px 8px', fontSize: '12px', backgroundColor: '#f44336', color: 'white', border: 'none', borderRadius: '3px', cursor: 'pointer' }, '清除选择'); deselectAllBtn.addEventListener('click', () => this.deselectAll()); // 将按钮添加到按钮容器 buttonsContainer.appendChild(refreshBtn); buttonsContainer.appendChild(deselectAllBtn); // 将章节数量和按钮容器添加到控制区 controlsContainer.appendChild(chapterCountLabel); controlsContainer.appendChild(buttonsContainer); this.chapterListContainer.appendChild(controlsContainer); // 添加章节列表 chapterElements.forEach((chapterElement, index) => { const chapterItem = this.createElement('div', { display: 'flex', alignItems: 'center', marginBottom: '5px' }); // 章节名称 const chapterName = this.createElement('span', { flex: 1 }, chapterElement.textContent.trim()); chapterItem.appendChild(chapterName); // 复选框 const checkbox = this.createElement('input', { type: 'checkbox', marginLeft: '10px' }); checkbox.addEventListener('change', () => this.toggleChapterSelection(index, checkbox)); chapterItem.appendChild(checkbox); this.chapterListContainer.appendChild(chapterItem); }); } // 添加取消选择模式的方法 cancelSelectionMode() { this.isSelectionMode = false; this.chapterListContainer.style.display = 'none'; this.cancelSelectionButton.style.display = 'none'; this.selectButton.textContent = '选择章节下载'; this.selectedChapters.clear(); } // 保留取消全选方法 deselectAll() { const checkboxes = this.chapterListContainer.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach(checkbox => { checkbox.checked = false; }); this.selectedChapters.clear(); this.selectButton.textContent = '选择章节下载'; } // 移除 selectAll、selectPrevious5Chapters、selectNext5Chapters 方法 selectAll(count) { const checkboxes = this.chapterListContainer.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach((checkbox, index) => { // 跳过第一行的控制按钮 if (index > 0) { checkbox.checked = true; this.selectedChapters.add(index - 1); // 减1是因为索引从0开始 } }); this.selectButton.textContent = `下载选中章节 (${count})`; } // 添加取消全选方法 // 添加取消全选方法 deselectAll() { const checkboxes = this.chapterListContainer.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach(checkbox => { checkbox.checked = false; }); this.selectedChapters.clear(); this.selectButton.textContent = '下载选中章节'; } // 添加刷新章节列表的方法 refreshChapterList() { console.log('刷新章节列表'); // 保存当前选中的章节 const selectedChapters = new Set(this.selectedChapters); // 重新初始化章节列表 this.initChapterList(); // 尝试恢复之前选中的章节(如果它们仍然存在) const checkboxes = this.chapterListContainer.querySelectorAll('input[type="checkbox"]'); const maxIndex = checkboxes.length - 1; selectedChapters.forEach(index => { if (index <= maxIndex) { // 跳过控制区域,所以索引需要+1 const checkbox = checkboxes[index + 1]; if (checkbox) { checkbox.checked = true; this.selectedChapters.add(index); } } }); // 更新按钮文本 if (this.selectedChapters.size > 0) { this.selectButton.textContent = `下载选中章节 (${this.selectedChapters.size})`; } else { this.selectButton.textContent = '下载选中章节'; } // 显示刷新成功提示 const chapterCountLabel = this.chapterListContainer.querySelector('span'); const originalText = chapterCountLabel.textContent; chapterCountLabel.textContent = '刷新成功!'; setTimeout(() => { chapterCountLabel.textContent = originalText; }, 1500); } // 修改切换选择方法,添加checkbox参数 toggleChapterSelection(index, checkbox) { if (this.selectedChapters.has(index)) { this.selectedChapters.delete(index); if (checkbox) checkbox.checked = false; } else { this.selectedChapters.add(index); if (checkbox) checkbox.checked = true; } // 更新按钮文本 if (this.selectedChapters.size > 0) { this.selectButton.textContent = `下载选中章节 (${this.selectedChapters.size})`; } else { this.selectButton.textContent = '下载选中章节'; } } // 添加 toggleSelectionMode 方法 toggleSelectionMode() { console.log('切换选择模式,当前状态:', this.isSelectionMode); this.isSelectionMode = !this.isSelectionMode; if (this.isSelectionMode) { // 每次进入选择模式时重新获取章节列表 console.log('进入选择模式,初始化章节列表'); this.initChapterList(); this.chapterListContainer.style.display = 'block'; this.cancelSelectionButton.style.display = 'block'; // 显示取消按钮 this.selectButton.textContent = '下载选中章节'; } else if (this.selectedChapters.size > 0) { // 当退出选择模式且有选中的章节时,触发下载 console.log('退出选择模式,开始下载选中章节'); this.chapterListContainer.style.display = 'none'; this.cancelSelectionButton.style.display = 'none'; // 隐藏取消按钮 this.onDownloadSelected(); } else { // 没有选中章节时只隐藏列表 console.log('退出选择模式,无选中章节'); this.chapterListContainer.style.display = 'none'; this.cancelSelectionButton.style.display = 'none'; // 隐藏取消按钮 this.selectButton.textContent = '选择章节下载'; } } // 添加 setLoading 方法 setLoading(isLoading, totalChapters = 0) { this.selectButton.disabled = isLoading; this.selectButton.style.backgroundColor = isLoading ? '#999' : '#4CAF50'; this.selectButton.style.cursor = isLoading ? 'not-allowed' : 'pointer'; if (isLoading) { this.selectButton.textContent = '下载中...'; this.chapterListContainer.style.display = 'none'; this.progressContainer.style.display = 'block'; // 设置进度条最大值 this.progressBar.max = totalChapters; this.progressBar.value = 0; this.progressText.textContent = `准备下载 ${totalChapters} 个章节...`; } else { this.selectButton.textContent = '选择章节下载'; this.progressContainer.style.display = 'none'; } } // 添加更新进度的方法 updateProgress(current, total) { if (this.progressBar) { this.progressBar.value = current; const percent = ((current / total) * 100).toFixed(2); this.progressText.textContent = `下载进度: ${current}/${total} (${percent}%)`; } } // ... 其他方法保持不变 ... } // 5. 下载器类 class ComicDownloader { constructor() { this.adapter = getSiteAdapter(); this.isLongPageMode = false; // 添加长图模式标志 console.log('当前页面URL:', window.location.href); // 判断当前页面类型 if (this.adapter.isChapterPage()) { console.log('检测到章节页面'); // 添加调试信息 const imageElements = this.adapter.getImageElements(); console.log('找到图片元素数量:', imageElements.length); // 添加调试信息 // 具体章节页面:初始化下载功能 this.totalPages = imageElements.length; this.chapterName = this.adapter.getChapterName(); console.log('章节名称:', this.chapterName); // 添加调试信息 if (this.totalPages > 0) { this.ui = new DownloaderUI(this.totalPages, this.handleDownload.bind(this), this.handleCancel.bind(this)); } else { console.error('未找到图片元素'); } } else if (this.adapter.isDirectoryPage()) { console.log('检测到目录页面'); // 添加调试信息 this.ui = new ChapterSelectorUI(this.handleDownloadSelected.bind(this)); } else { console.log('当前页面不支持下载功能'); } } // 处理下载选中章节 // 在 ComicDownloader 类中添加新方法 async loadChapterHtml(url) { return new Promise((resolve, reject) => { const tab = GM_openInTab(url, { active: false, insert: true }); // 创建一个消息监听器 const messageHandler = function (event) { if (event.data.type === 'chapterData' && event.data.url === url) { window.removeEventListener('message', messageHandler); tab.close(); resolve(event.data.html); } }; window.addEventListener('message', messageHandler); // 5秒后超时 setTimeout(() => { window.removeEventListener('message', messageHandler); tab.close(); reject(new Error('加载章节超时')); }, 5000); }); } async handleDownloadSelected() { const selectedChapters = this.ui.selectedChapters; if (selectedChapters.size === 0) { this.ui.selectButton.textContent = '请选择至少一个章节'; setTimeout(() => { this.ui.selectButton.textContent = '选择章节下载'; }, 2000); return; } try { const chapterLinks = this.adapter.getChapterLinks(); const selectedChapterUrls = Array.from(selectedChapters).map(index => chapterLinks[index]); this.ui.setLoading(true, selectedChapterUrls.length); for (let i = 0; i < selectedChapterUrls.length; i++) { const url = selectedChapterUrls[i]; try { console.log(`开始下载章节 ${i + 1}/${selectedChapterUrls.length}: ${url}`); const sessionId = Date.now().toString(); GM_setValue('autoDownload', true); GM_setValue('sessionId', sessionId); GM_setValue('downloadStatus', 'pending'); // 修改:使用 active: true 打开标签页 const tab = GM_openInTab(url, { active: true, // 修改为 true,确保标签页处于活动状态 insert: true, setParent: true }); // 等待下载完成,增加重试机制 await new Promise((resolve, reject) => { const maxRetries = 3; // 最大重试次数 let retryCount = 0; let timeout; const checkStatus = () => { const status = GM_getValue('downloadStatus', ''); console.log(`检查下载状态: ${status}, 重试次数: ${retryCount}`); if (status === 'complete') { clearTimeout(timeout); GM_setValue('downloadStatus', ''); GM_setValue('autoDownload', false); resolve(); return true; } return false; }; const startCheck = () => { timeout = setTimeout(() => { if (!checkStatus() && retryCount < maxRetries) { console.log(`下载超时,尝试重试 ${retryCount + 1}/${maxRetries}`); retryCount++; // 重新激活标签页 tab.activate(); startCheck(); } else if (retryCount >= maxRetries) { GM_setValue('downloadStatus', ''); reject(new Error('下载超时,已达到最大重试次数')); } }, 30000); // 30秒超时 }; // 开始检查 startCheck(); // 定期检查状态 const checkInterval = setInterval(() => { if (checkStatus()) { clearInterval(checkInterval); } }, 1000); }); tab.close(); this.ui.updateProgress(i + 1, selectedChapterUrls.length); } catch (error) { console.error(`章节下载失败: ${url}`, error); } } this.ui.setLoading(false); this.ui.selectButton.textContent = '下载完成!'; setTimeout(() => { this.ui.selectButton.textContent = '选择章节下载'; }, 3000); } catch (error) { console.error('批量下载失败:', error); this.ui.setLoading(false); this.ui.selectButton.textContent = '下载失败,请查看控制台'; setTimeout(() => { this.ui.selectButton.textContent = '选择章节下载'; }, 3000); } } // 从文档中提取图片URL extractImageUrlsFromDoc(doc) { const imageElements = doc.querySelectorAll('div.chapter-img-box img'); return Array.from(imageElements).map(img => img.src || img.dataset.src); } // 下载单个章节的所有图片 async downloadChapterImages(imageUrls) { const images = []; for (const url of imageUrls) { try { const imageData = await this.downloadImage(url); images.push(imageData); } catch (error) { console.error(`图片下载失败: ${url}`, error); } } return images; } async handleDownload() { if (this.isDownloading) { alert('当前正在下载,请稍后再试'); return; } try { this.isDownloading = true; this.abortController = new AbortController(); this.ui.setLoading(true, true); // 传递长图模式状态 this.isLongPageMode = this.ui.isLongPageMode; await this.downloadComic(); } catch (error) { if (error.name === 'AbortError') { console.log('下载已取消'); alert('下载已取消'); } else { this.handleError(error, '下载失败'); } } finally { this.isDownloading = false; this.abortController = null; this.ui.setLoading(false, false); } } handleCancel() { if (this.abortController) { this.abortController.abort(); // 中断下载 this.isDownloading = false; // 重置下载状态 this.ui.setLoading(false, false); // 重置UI状态 // 显示取消消息,然后恢复下载按钮 setTimeout(() => { this.ui.downloadButton.style.display = 'block'; this.ui.downloadButton.disabled = false; }, 2000); } } async downloadComic() { const images = await this.downloadImages(1, this.totalPages); await this.generatePDF(images); } 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 pageNumber = i + 1; if (pageNumber >= start && pageNumber <= end) { this.addDownloadPromise(imageElements[i], pageNumber, start, downloadResults, downloadPromises); } } await Promise.all(downloadPromises); return downloadResults; } addDownloadPromise(element, pageNumber, start, downloadResults, downloadPromises) { const imgUrl = this.adapter.getImageUrl(element); if (imgUrl) { const arrayIndex = pageNumber - start; downloadPromises.push( this.downloadImage(imgUrl) .then(imgData => { downloadResults[arrayIndex] = imgData; this.ui.updateProgress(pageNumber); }) .catch(error => { console.error(`第 ${pageNumber} 页下载失败:`, error); downloadResults[arrayIndex] = null; }) ); } } downloadImage(url) { return new Promise((resolve, reject) => { if (this.abortController?.signal?.aborted) { reject(new DOMException('下载已取消', 'AbortError')); return; } const request = GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'blob', onload: response => this.handleImageResponse(response, resolve, reject), onerror: error => reject(error) }); // 监听中断信号 this.abortController?.signal?.addEventListener('abort', () => { request.abort(); // 中断请求 reject(new DOMException('下载已取消', 'AbortError')); }); }); } 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(); const sizes = await this.getImageSizes(images); if (this.isLongPageMode) { // 长图模式:将所有图片垂直拼接 await this.generateLongPagePDF(pdf, images, sizes); } else { // 普通模式:每页一张图片 for (let i = 0; i < images.length; i++) { await this.addImageToPdf(pdf, images[i], i, sizes[i]); this.ui.updateProgress(i + 1); } } pdf.save(`${this.chapterName}.pdf`); } async generateLongPagePDF(pdf, images, sizes) { // 计算所有图片的总高度 const A4_width = 210; let totalHeight = 0; let maxWidth = 0; // 计算缩放比例和总高度 for (let i = 0; i < sizes.length; i++) { const scaleFactor = A4_width / sizes[i].width; const scaledHeight = sizes[i].height * scaleFactor; totalHeight += scaledHeight; maxWidth = Math.max(maxWidth, sizes[i].width * scaleFactor); } // 设置PDF页面大小 pdf.internal.pageSize.width = A4_width; pdf.internal.pageSize.height = totalHeight; // 垂直拼接所有图片 let currentY = 0; for (let i = 0; i < images.length; i++) { const img = new Image(); img.src = images[i]; await new Promise(resolve => { img.onload = () => { const scaleFactor = A4_width / sizes[i].width; const scaledHeight = sizes[i].height * scaleFactor; pdf.addImage( images[i], 'JPEG', 0, currentY, A4_width, scaledHeight ); currentY += scaledHeight; this.ui.updateProgress(i + 1); resolve(); }; }); } } async getImageSizes(images) { return Promise.all(images.map(imgData => { return new Promise(resolve => { const img = new Image(); img.src = imgData; img.onload = () => resolve({ width: img.width, height: img.height }); }); })); } async addImageToPdf(pdf, imgData, index, size) { return new Promise(resolve => { const img = new Image(); img.src = imgData; img.onload = () => { if (index > 0) pdf.addPage(); const A4_width = 210; const A4_height = 297; const scaleFactor = A4_width / size.width; let finalWidth = A4_width; let finalHeight = size.height * scaleFactor; if (finalHeight > A4_height) { finalHeight = A4_height; finalWidth = size.width * (A4_height / size.height); } pdf.internal.pageSize.width = finalWidth; pdf.internal.pageSize.height = finalHeight; pdf.addImage(imgData, 'JPEG', 0, 0, finalWidth, finalHeight); resolve(); }; }); } handleError(error, message = '下载失败') { console.error(message, error); alert(`${message},请查看控制台了解详情`); } } // 6. 初始化 function initialize() { console.log('开始初始化下载器...'); try { // 创建下载器实例 window.comicDownloader = new ComicDownloader(); // 检查是否需要自动下载 const autoDownload = GM_getValue('autoDownload', false); console.log('自动下载标志:', autoDownload); // 如果是章节页面且需要自动下载 if (autoDownload && window.comicDownloader.adapter.isChapterPage()) { console.log('检测到是从目录页面打开的章节页面,准备自动下载'); // 延迟一段时间后自动开始下载 setTimeout(async () => { try { console.log('开始自动下载...'); await window.comicDownloader.handleDownload(); console.log('自动下载完成,设置状态为 complete'); // 设置下载完成状态 GM_setValue('downloadStatus', 'complete'); } catch (error) { console.error('自动下载失败:', error); // 即使失败也设置完成状态,避免父窗口一直等待 GM_setValue('downloadStatus', 'complete'); } }, 2000); } } catch (error) { console.error('初始化失败:', error); } } // 确保在页面完全加载后再初始化 if (document.readyState === 'complete') { initialize(); } else { window.addEventListener('load', initialize); } })();