// ==UserScript== // @name PO18小说下载器 // @namespace http://tampermonkey.net/ // @version 1.7.0 // @description 下载PO18小说,支持TXT/HTML/EPUB格式,多线程下载,记录下载历史,增强阅读体验,查看已购书架,WebDAV上传 // @author wenmoux // @license MIT // @match https://www.po18.tw/* // @icon https://www.po18.tw/favicon.ico // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant unsafeWindow // @connect www.po18.tw // @connect * // @require https://unpkg.com/file-saver@2.0.5/dist/FileSaver.min.js // @require https://unpkg.com/jszip@3.10.1/dist/jszip.min.js // @downloadURL https://update.greasyfork.icu/scripts/534737/PO18%E5%B0%8F%E8%AF%B4%E4%B8%8B%E8%BD%BD%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/534737/PO18%E5%B0%8F%E8%AF%B4%E4%B8%8B%E8%BD%BD%E5%99%A8.meta.js // ==/UserScript== (function() { 'use strict'; // ==== 轻量ZIP生成器 ==== class MiniZip { constructor() { this.files = []; } file(name, content, options = {}) { const data = typeof content === 'string' ? new TextEncoder().encode(content) : new Uint8Array(content); this.files.push({ name, data, options }); return this; } async generateAsync(options) { const files = this.files; const parts = []; const centralDir = []; let offset = 0; for (const file of files) { const nameBytes = new TextEncoder().encode(file.name); const crc = this._crc32(file.data); const size = file.data.length; // Local file header (30 bytes + filename) const header = new ArrayBuffer(30); const hv = new DataView(header); hv.setUint32(0, 0x04034b50, true); // 签名 hv.setUint16(4, 20, true); // 版本 hv.setUint16(6, 0, true); // 标志 hv.setUint16(8, 0, true); // 压缩方法: STORE hv.setUint16(10, 0, true); // 修改时间 hv.setUint16(12, 0x21, true); // 修改日期 hv.setUint32(14, crc, true); // CRC32 hv.setUint32(18, size, true); // 压缩后大小 hv.setUint32(22, size, true); // 原始大小 hv.setUint16(26, nameBytes.length, true); // 文件名长度 hv.setUint16(28, 0, true); // 额外字段长度 parts.push(new Uint8Array(header), nameBytes, file.data); // Central directory entry (46 bytes + filename) const cd = new ArrayBuffer(46); const cv = new DataView(cd); cv.setUint32(0, 0x02014b50, true); // 签名 cv.setUint16(4, 20, true); // 创建版本 cv.setUint16(6, 20, true); // 需要版本 cv.setUint16(8, 0, true); // 标志 cv.setUint16(10, 0, true); // 压缩方法: STORE cv.setUint16(12, 0, true); // 修改时间 cv.setUint16(14, 0x21, true); // 修改日期 cv.setUint32(16, crc, true); // CRC32 cv.setUint32(20, size, true); // 压缩后大小 cv.setUint32(24, size, true); // 原始大小 cv.setUint16(28, nameBytes.length, true); // 文件名长度 cv.setUint16(30, 0, true); // 额外字段长度 cv.setUint16(32, 0, true); // 文件注释长度 cv.setUint16(34, 0, true); // 磁盘编号 cv.setUint16(36, 0, true); // 内部属性 cv.setUint32(38, 0, true); // 外部属性 cv.setUint32(42, offset, true); // 本地文件头偏移 centralDir.push(new Uint8Array(cd), nameBytes); offset += 30 + nameBytes.length + size; } // End of central directory const cdOffset = offset; let cdSize = 0; centralDir.forEach(arr => cdSize += arr.length); const eocd = new ArrayBuffer(22); const ev = new DataView(eocd); ev.setUint32(0, 0x06054b50, true); // 签名 ev.setUint16(4, 0, true); // 磁盘编号 ev.setUint16(6, 0, true); // 开始磁盘 ev.setUint16(8, files.length, true); // 此盘记录数 ev.setUint16(10, files.length, true); // 总记录数 ev.setUint32(12, cdSize, true); // 中心目录大小 ev.setUint32(16, cdOffset, true); // 中心目录偏移 ev.setUint16(20, 0, true); // 注释长度 // 合并所有部分 const allParts = [...parts, ...centralDir, new Uint8Array(eocd)]; const totalSize = allParts.reduce((sum, arr) => sum + arr.length, 0); const result = new Uint8Array(totalSize); let pos = 0; allParts.forEach(arr => { result.set(arr, pos); pos += arr.length; }); return new Blob([result], { type: 'application/epub+zip' }); } _crc32(data) { if (!MiniZip._crcTable) { const table = new Uint32Array(256); for (let i = 0; i < 256; i++) { let c = i; for (let j = 0; j < 8; j++) { c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); } table[i] = c; } MiniZip._crcTable = table; } let crc = 0xFFFFFFFF; for (let i = 0; i < data.length; i++) { crc = MiniZip._crcTable[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8); } return (crc ^ 0xFFFFFFFF) >>> 0; } } // ==== 外部库引用 ==== const _JSZip = MiniZip; const _saveAs = (typeof saveAs !== 'undefined') ? saveAs : (typeof window.saveAs !== 'undefined') ? window.saveAs : (typeof unsafeWindow !== 'undefined' && unsafeWindow.saveAs) ? unsafeWindow.saveAs : null; // ==== 工具函数 ==== const $ = (sel, ctx = document) => ctx.querySelector(sel); const $$ = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel)); const create = (tag, attrs = {}, html = '') => { const el = document.createElement(tag); Object.entries(attrs).forEach(([k, v]) => k === 'className' ? el.className = v : el.setAttribute(k, v)); if (html) el.innerHTML = html; return el; }; // HTML解析器 const HTMLParser = { parse(html) { const doc = new DOMParser().parseFromString(html, 'text/html'); return { $: sel => doc.querySelector(sel), $$: sel => Array.from(doc.querySelectorAll(sel)), text: sel => doc.querySelector(sel)?.textContent.trim() || '', attr: (sel, attr) => doc.querySelector(sel)?.getAttribute(attr), getText: () => doc.body.textContent, getHTML: () => doc.body.innerHTML, remove(sel) { doc.querySelectorAll(sel).forEach(el => el.remove()); return this; }, // 兼容旧API querySelector: sel => doc.querySelector(sel), querySelectorAll: sel => Array.from(doc.querySelectorAll(sel)), getTextContent: sel => doc.querySelector(sel)?.textContent.trim() || '', getAttributeValue: (sel, attr) => doc.querySelector(sel)?.getAttribute(attr) }; } }; // ==== 样式设置 - 修改为淡粉色主题 ==== GM_addStyle(` /* 粉色主题风格 */ :root { --primary-color: #FF8BA7; /* 主色调修改为淡粉色 */ --primary-light: #FFB2C0; /* 浅色调 */ --primary-dark: #D46A87; /* 深色调 */ --text-on-primary: #ffffff; --surface-color: #ffffff; --background-color: #FFF0F3; --error-color: #D32F2F; --box-shadow: 0 2px 4px rgba(0,0,0,.1), 0 3px 6px rgba(0,0,0,.05); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); } .po18-downloader { font-family: 'Roboto', sans-serif; color: #333; } .po18-float-button { position: fixed; bottom: 30px; right: 30px; width: 56px; height: 56px; border-radius: 50%; background-color: var(--primary-color); color: var(--text-on-primary); display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 3px 5px rgba(0,0,0,0.3); z-index: 9999; user-select: none; transition: var(--transition); } .po18-float-button:hover { transform: scale(1.1);box-shadow: 0 5px 8px rgba(0,0,0,0.3); } .po18-panel { position: fixed; bottom: 100px; right: 30px; width: 360px; background-color: var(--surface-color); border-radius: 12px; box-shadow: var(--box-shadow); z-index: 9998; overflow: hidden; display: none; max-height: 600px; transition: var(--transition); } .po18-panel.active { display: block; } .po18-header { background-color: var(--primary-color); color: var(--text-on-primary); padding: 16px; font-weight: 500; font-size: 18px; display: flex; justify-content: space-between;align-items: center; } .po18-tabs { display: flex; background-color: var(--primary-light); color: var(--text-on-primary); } .po18-tab { flex: 1; text-align: center; padding: 12px 0; cursor: pointer; transition: var(--transition); border-bottom: 3px solid transparent;} .po18-tab.active { border-bottom: 3px solid white; background-color: var(--primary-color); } .po18-tab:hover:not(.active) { background-color: rgba(255,255,255,0.1); } .po18-tab-content { padding: 16px; max-height: 450px; overflow-y: auto; } .po18-tab-pane { display: none; } .po18-tab-pane.active { display: block; } .po18-card { background-color: white; border-radius: 12px; padding: 16px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } /* 书籍详情样式 */ .po18-book-info { display: flex; margin-bottom: 15px; } .po18-book-cover { width: 100px; height: 140px; object-fit: cover; border-radius: 6px; margin-right: 15px; } .po18-book-details { flex: 1;} .po18-book-title { font-size: 18px; font-weight: bold; margin-bottom: 6px; color: #333; } .po18-book-author { font-size: 14px; color: #666; margin-bottom: 10px; } .po18-book-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px; } .po18-book-tag { background-color: var(--primary-light); color: #333; padding: 2px 8px; border-radius: 10px; font-size: 12px;} .po18-form-group { margin-bottom: 12px; } .po18-form-group label { display: block; margin-bottom: 5px; font-weight:500; color: #666; } .po18-select { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px; background-color: white; } .po18-button { padding: 10px 16px; border: none; border-radius: 8px; background-color: var(--primary-color); color: white; cursor: pointer; font-weight: 500; transition: var(--transition); } .po18-button:hover { background-color: var(--primary-dark); }.po18-button:disabled { background-color: #cccccc; cursor: not-allowed; } .po18-progress { height: 8px; background-color: #eee; border-radius: 4px; margin: 10px 0;overflow: hidden; } .po18-progress-bar { height: 100%; background-color: var(--primary-color); width: 0%;transition: width 0.3s ease; } .po18-log { font-family: monospace; background-color: #f8f8f8; padding: 10px; border-radius: 8px; max-height: 200px; overflow-y: auto; font-size: 12px; white-space: pre-wrap;} .po18-record-item { padding: 12px; border-left: 4px solid var(--primary-color); background-color: #f9f9f9; margin-bottom: 10px; border-radius: 08px 8px 0; } .po18-record-item h4 { margin: 0 0 8px 0;} .po18-record-info { display: flex; justify-content: space-between; font-size: 12px; color: #666; } /*拖动样式 */ .po18-draggable { cursor: move; } /* 书架相关样式 */ .po18-bookshelf-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .po18-bookshelf-header h3 { margin: 0; color: var(--primary-dark); } .po18-bookshelf-status { font-size: 14px; color: #666; margin-bottom: 15px; } .po18-book-item { border-bottom: 1px solid #eee; padding: 15px 0; } .po18-book-item:last-child { border-bottom: none; } .po18-book-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 10px; } .po18-button-small { padding: 5px 10px; font-size: 12px; } .po18-empty-message { text-align: center; padding: 30px 0; color: #666; } .po18-book-year { font-size: 12px; color: #888; margin-top: 5px; } /* WebDAV设置样式 */ .po18-input { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 8px; background-color: white; margin-bottom: 8px; box-sizing: border-box; } .po18-input:focus { outline: none; border-color: var(--primary-color); } .po18-checkbox-group { display: flex; align-items: center; margin-bottom: 12px; } .po18-checkbox-group input { margin-right: 8px; } .po18-status { padding: 8px; border-radius: 6px; margin-top: 10px; font-size: 12px; } .po18-status.success { background: #E8F5E9; color: #2E7D32; } .po18-status.error { background: #FFEBEE; color: #C62828; } .po18-status.info { background: #E3F2FD; color: #1565C0; } `); // ==== 主要功能实现 ==== const Po18Downloader = { content: [], option: {}, logs: [], downloadRecords: GM_getValue('downloadRecords', []), currentTab: 'download', bid: null, downloadFormat: 'txt', threadCount: 3, isDownloading: false, totalChapters: 0, downloadedChapters: 0, startTime: 0, // WebDAV配置 webdavConfig: GM_getValue('webdavConfig', { enabled: false, url: '', username: '', password: '', path: '/books/' }), lastDownloadedFile: null, // 保存最后下载的文件信息 init() { this.createUI(); this.bindEvents(); this.loadSettings(); this.detectNovelPage(); // 检查登录状态 this.checkLoginStatus(); }, createUI() { // 创建悬浮按钮 const floatButton = document.createElement('div'); floatButton.className = 'po18-float-button'; floatButton.innerHTML = ''; document.body.appendChild(floatButton); // 创建主面板 const panel = document.createElement('div'); panel.className = 'po18-panel'; // 使用模板字符串确保HTML格式正确 panel.innerHTML = `
PO18小说下载器
下载
日志
记录
设置
关于

我的书架

加载中...

WebDAV 设置

PO18小说下载器增强版 v1.6.0

这是一款用于下载PO18网站小说的工具,支持TXT/HTML/EPUB格式下载,WebDAV上传等功能。

作者github:wenmoux:

新增功能:

  1. 全新的粉色主题界面
  2. 显示小说封面、作者和标签
  3. 增强HTML输出,支持电子书式的左右翻页
  4. 阅读界面支持字体大小、颜色主题调整
  5. 新增行间距、字间距调整功能
  6. 优化正文排版和阅读舒适度
  7. 新增书架功能,便于管理已购买小说
  8. epub下载
  9. webdav上传

使用方法:

  1. 在小说页面点击悬浮按钮
  2. 选择下载格式和线程数
  3. 点击"开始下载"按钮

注意:需要先登录PO18网站才能下载已购买的章节。

`; document.body.appendChild(panel); }, bindEvents() { // 点击悬浮按钮显示/隐藏面板 document.querySelector('.po18-float-button').addEventListener('click', () => { const panel = document.querySelector('.po18-panel'); panel.classList.toggle('active'); }); // 点击关闭按钮 document.getElementById('po18-close').addEventListener('click', () => { document.querySelector('.po18-panel').classList.remove('active'); }); // 标签页切换 document.querySelectorAll('.po18-tab').forEach(tab => { tab.addEventListener('click', (e) => { this.currentTab = e.target.dataset.tab; // 移除所有标签的active类 document.querySelectorAll('.po18-tab').forEach(t => { t.classList.remove('active'); }); // 移除所有面板的active类 document.querySelectorAll('.po18-tab-pane').forEach(p => { p.classList.remove('active'); }); // 添加当前标签和面板的active类 e.target.classList.add('active'); const pane = document.getElementById(`po18-tab-${this.currentTab}`); if (pane) { pane.classList.add('active'); } if (this.currentTab === 'records') { this.renderDownloadRecords(); } else if (this.currentTab === 'bookshelf') { this.renderBookshelf(); } }); }); // 下载按钮 document.getElementById('po18-start').addEventListener('click', () => { this.startDownload(); }); // 下载格式选择 document.getElementById('po18-format').addEventListener('change', (e) => { this.downloadFormat = e.target.value; GM_setValue('downloadFormat', this.downloadFormat); }); // 线程数选择 document.getElementById('po18-thread').addEventListener('change', (e) => { this.threadCount = parseInt(e.target.value); GM_setValue('threadCount', this.threadCount); }); // 书架刷新按钮事件 document.getElementById('po18-refresh-bookshelf')?.addEventListener('click', () => { this.log('正在刷新书架数据...'); this.fetchBookshelf().then(books => { this.getBookDetails(books).then(detailedBooks => { this.renderBookshelf(detailedBooks); }); }); }); // 实现悬浮按钮的拖动功能 this.makeDraggable(document.querySelector('.po18-float-button')); // 实现面板的拖动功能 this.makeDraggable(document.querySelector('.po18-panel'), document.querySelector('.po18-draggable')); // WebDAV事件绑定 this.bindWebDAVEvents(); }, bindWebDAVEvents() { // 加载WebDAV配置到表单 const config = this.webdavConfig; const enabledEl = document.getElementById('po18-webdav-enabled'); const urlEl = document.getElementById('po18-webdav-url'); const usernameEl = document.getElementById('po18-webdav-username'); const passwordEl = document.getElementById('po18-webdav-password'); const pathEl = document.getElementById('po18-webdav-path'); if (enabledEl) enabledEl.checked = config.enabled; if (urlEl) urlEl.value = config.url; if (usernameEl) usernameEl.value = config.username; if (passwordEl) passwordEl.value = config.password; if (pathEl) pathEl.value = config.path; // 保存配置 document.getElementById('po18-webdav-save')?.addEventListener('click', () => { this.webdavConfig = { enabled: enabledEl?.checked || false, url: urlEl?.value.trim() || '', username: usernameEl?.value.trim() || '', password: passwordEl?.value || '', path: pathEl?.value.trim() || '/books/' }; GM_setValue('webdavConfig', this.webdavConfig); this.showWebDAVStatus('配置已保存', 'success'); this.log('WebDAV配置已保存'); }); // 测试连接 document.getElementById('po18-webdav-test')?.addEventListener('click', () => { this.testWebDAVConnection(); }); }, showWebDAVStatus(message, type = 'info') { const statusEl = document.getElementById('po18-webdav-status'); if (statusEl) { statusEl.className = 'po18-status ' + type; statusEl.textContent = message; setTimeout(() => { statusEl.textContent = ''; statusEl.className = ''; }, 3000); } }, testWebDAVConnection() { const config = this.webdavConfig; if (!config.url) { this.showWebDAVStatus('请先填写服务器地址', 'error'); return; } this.showWebDAVStatus('正在测试连接...', 'info'); GM_xmlhttpRequest({ method: 'PROPFIND', url: config.url.replace(/\/$/, '') + config.path, headers: { 'Authorization': 'Basic ' + btoa(config.username + ':' + config.password), 'Depth': '0' }, onload: (response) => { if (response.status >= 200 && response.status < 300) { this.showWebDAVStatus('✅ 连接成功!', 'success'); this.log('WebDAV连接测试成功'); } else if (response.status === 404) { this.showWebDAVStatus('⚠️ 路径不存在,将在上传时自动创建', 'info'); } else if (response.status === 401) { this.showWebDAVStatus('❌ 认证失败,请检查用户名密码', 'error'); } else { this.showWebDAVStatus('❌ 连接失败: ' + response.status, 'error'); } }, onerror: (error) => { this.showWebDAVStatus('❌ 网络错误,请检查地址', 'error'); this.log('WebDAV连接失败: ' + (error.message || '网络错误')); } }); }, // 上传文件到WebDAV async uploadToWebDAV(blob, fileName) { const config = this.webdavConfig; if (!config.enabled || !config.url) { return false; } this.log('正在上传到WebDAV: ' + fileName); return new Promise((resolve) => { const fullPath = config.url.replace(/\/$/, '') + config.path.replace(/\/$/, '') + '/' + fileName; GM_xmlhttpRequest({ method: 'PUT', url: fullPath, headers: { 'Authorization': 'Basic ' + btoa(config.username + ':' + config.password), 'Content-Type': 'application/octet-stream' }, data: blob, onload: (response) => { if (response.status >= 200 && response.status < 300) { this.log('WebDAV上传成功: ' + fileName); resolve(true); } else { this.log('WebDAV上传失败: ' + response.status); resolve(false); } }, onerror: (error) => { this.log('WebDAV上传错误: ' + (error.message || '网络错误')); resolve(false); } }); }); }, makeDraggable(element, handle = null) { const dragElement = handle || element; let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; dragElement.addEventListener('mousedown', dragMouseDown); function dragMouseDown(e) { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.addEventListener('mouseup', closeDragElement); document.addEventListener('mousemove', elementDrag); } function elementDrag(e) { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; const newTop = element.offsetTop - pos2; const newLeft = element.offsetLeft - pos1; // 确保元素不会被拖出可视区域 if (newTop > 0 && newTop < window.innerHeight - element.offsetHeight) { element.style.top = newTop + "px"; } if (newLeft > 0 && newLeft < window.innerWidth - element.offsetWidth) { element.style.left = newLeft + "px"; } } function closeDragElement() { document.removeEventListener('mouseup', closeDragElement); document.removeEventListener('mousemove', elementDrag); } }, loadSettings() { this.downloadFormat = GM_getValue('downloadFormat', 'txt'); this.threadCount = GM_getValue('threadCount', 3); const formatSelect = document.getElementById('po18-format'); const threadSelect = document.getElementById('po18-thread'); if (formatSelect) formatSelect.value = this.downloadFormat; if (threadSelect) threadSelect.value = this.threadCount.toString(); }, detectNovelPage() { const url = window.location.href; const bidMatch = url.match(/\/books\/(\d+)/); if (bidMatch) { this.bid = bidMatch[1]; this.log(`检测到小说ID: ${this.bid}`); // 获取小说信息并显示 this.fetchBookDetails(this.bid); } else { this.log('未检测到小说页面'); } }, // 检查登录状态 checkLoginStatus() { // 检查页面中是否包含"登入"文字,如果没有则认为已登录 const pageContent = document.body.textContent || ''; const isLoggedIn = !pageContent.includes('登入'); // 显示或隐藏书架标签 const bookshelfTab = document.getElementById('po18-bookshelf-tab'); if (bookshelfTab) { bookshelfTab.style.display = isLoggedIn ? 'block' : 'none'; } return isLoggedIn; }, // 获取已购书架数据 async fetchBookshelf() { if (!this.checkLoginStatus()) { this.log('未登录,无法获取书架信息'); return []; } const allBooks = []; const currentYear = new Date().getFullYear(); // 获取最近5年的书籍 for (let year = currentYear; year >= currentYear - 5; year--) { try { const yearBooks = await this.fetchBookshelfByYear(year); if (yearBooks.length) { allBooks.push(...yearBooks); } } catch (error) { this.log(`获取${year}年书籍失败: ${error.message || '未知错误'}`); } } // 缓存书籍信息 GM_setValue('bookshelfData', { books: allBooks, timestamp: Date.now() }); return allBooks; }, async fetchBookshelfByYear(year) { return new Promise((resolve) => { const url = `https://www.po18.tw/panel/stock_manage/buyed_lists?sort=order&date_year=${year}`; GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'referer': 'https://www.po18.tw', }, onload: (response) => { try { const html = response.responseText; const $ = HTMLParser.parse(html); const books = []; $.querySelectorAll('tbody>.alt-row').forEach((book) => { const nameEl = book.querySelector('a'); if (!nameEl) return; const name = nameEl.textContent.trim(); const href = nameEl.getAttribute('href'); const authorEl = book.querySelector('.T_author'); // 从href中提取bid const bidMatch = href ? href.match(/\/books\/(\d+)/) : null; const bid = bidMatch ? bidMatch[1] : null; if (name && bid) { books.push({ title: name, bid: bid, author: authorEl ? authorEl.textContent.trim() : '未知作者', cover: null, // 稍后会通过详情获取 detail: `https://www.po18.tw${href}`, year: year }); } }); this.log(`获取到${year}年已购书籍 ${books.length} 本`); resolve(books); } catch (err) { this.log(`解析${year}年书籍列表失败: ${err.message || '未知错误'}`); resolve([]); } }, onerror: (error) => { this.log(`获取${year}年书籍列表请求失败: ${error.message || "未知错误"}`); resolve([]); } }); }); }, // 获取书籍详情并更新缓存 async getBookDetails(books) { const bookDetailsCache = GM_getValue('bookDetailsCache', {}); const now = Date.now(); const cacheExpiry = 7 * 24 * 60 * 60 * 1000; // 7天缓存过期 // 过滤出需要获取详情的书籍 const booksToFetch = books.filter(book => { const cachedBook = bookDetailsCache[book.bid]; return !cachedBook || (now - cachedBook.timestamp > cacheExpiry); }); if (booksToFetch.length === 0) { // 全部使用缓存 return books.map(book => { const cachedData = bookDetailsCache[book.bid]?.details; if (cachedData) { return { ...book, ...cachedData }; } return book; }); } // 分批获取详情,避免过多请求 const batchSize = 3; let processedCount = 0; for (let i = 0; i < booksToFetch.length; i += batchSize) { const batch = booksToFetch.slice(i, i + batchSize); await Promise.all(batch.map(async (book) => { try { const details = await this.getDetail(book.bid); if (details) { // 更新缓存 bookDetailsCache[book.bid] = { timestamp: now, details: { title: details.title, author: details.author, cover: details.cover, tags: details.tags } }; // 更新书籍数据 book.title = details.title; book.author = details.author; book.cover = details.cover; book.tags = details.tags; } processedCount++; this.log(`获取书籍详情 (${processedCount}/${booksToFetch.length}): ${book.title}`); // 更新界面 this.renderBookshelf(books); } catch (error) { this.log(`获取书籍 [${book.title}] 详情失败: ${error.message || '未知错误'}`); } })); // 短暂延迟,避免请求过快 if (i + batchSize < booksToFetch.length) { await new Promise(resolve => setTimeout(resolve, 1000)); } } // 保存缓存 GM_setValue('bookDetailsCache', bookDetailsCache); return books; }, // 渲染书架UI async renderBookshelf(books = null) { const container = document.getElementById('po18-bookshelf-container'); const statusEl = document.getElementById('po18-bookshelf-status'); if (!container) return; // 如果没有提供书籍列表,尝试从缓存加载 if (!books) { const cachedData = GM_getValue('bookshelfData', null); if (cachedData && Date.now() - cachedData.timestamp < 24 * 60 * 60 * 1000) { // 缓存不超过24小时 books = cachedData.books; this.log('从缓存加载书架数据'); } else { // 缓存过期或不存在,重新获取 if (statusEl) statusEl.textContent = '正在获取书架数据...'; books = await this.fetchBookshelf(); } // 获取书籍详情 books = await this.getBookDetails(books); } // 更新状态信息 if (statusEl) { statusEl.textContent = `共 ${books.length} 本已购书籍`; } // 渲染书架 let html = ''; if (books.length === 0) { html = '
没有找到已购书籍,请确认已登录PO18网站
'; } else { books.forEach((book) => { // 默认封面图 const coverUrl = book.cover || 'https://imgfzone.tooopen.com/20201106/tooopen_v11011311323157.jpg'; // 标签HTML let tagsHTML = ''; if (book.tags) { const tagsList = book.tags.split('·'); tagsList.forEach(tag => { if (tag.trim()) { tagsHTML += `${tag.trim()}`; } }); } html += `
${book.title}封面

${book.title}

作者: ${book.author}
${tagsHTML}
购买年份: ${book.year}
查看
`; }); } container.innerHTML = html; // 绑定下载按钮事件 document.querySelectorAll('.po18-download-book').forEach(button => { button.addEventListener('click', (e) => { const bid = e.target.dataset.bid; const title = e.target.dataset.title; if (bid) { this.bid = bid; this.log(`选择下载书籍: ${title} (${bid})`); // 切换到下载标签页document.querySelector('.po18-tab[data-tab="download"]').click(); // 获取书籍详情 this.fetchBookDetails(bid); } }); }); }, // 获取并显示小说详情 async fetchBookDetails(bid) { try { const detail = await this.getDetail(bid); if (detail) { this.renderBookDetails(detail); } } catch (err) { this.log(`获取小说详情失败: ${err.message || '未知错误'}`); } }, // 渲染小说详情 renderBookDetails(detail) { const container = document.getElementById('po18-book-details-container'); if (!container) return; // 标签HTML let tagsHTML = ''; if (detail.tags) { const tagsList = detail.tags.split('·'); tagsList.forEach(tag => { if (tag.trim()) { tagsHTML += `${tag.trim()}`; } }); } // 构造小说详情HTML const html = `
${detail.title}封面

${detail.title}

作者: ${detail.author}
${tagsHTML}
`; container.innerHTML = html; }, log(message) { const timestamp = new Date().toLocaleTimeString(); const logMessage = `[${timestamp}] ${message}`; this.logs.unshift(logMessage); // 限制日志数量 if (this.logs.length > 100) { this.logs.pop(); } // 更新日志显示 const logElement = document.getElementById('po18-logs'); if (logElement) { logElement.innerText = this.logs.join('\n'); } console.log(`[PO18下载器] ${message}`); }, updateProgress(current, total) { this.downloadedChapters = current; this.totalChapters = total; const percent = total > 0 ? Math.floor((current / total) * 100) : 0; const progressBar = document.getElementById('po18-progress'); const progressText = document.getElementById('po18-progress-text'); const downloadTime = document.getElementById('po18-download-time'); if (progressBar) progressBar.style.width = `${percent}%`; if (progressText) progressText.innerText = `${current}/${total} 章节 (${percent}%)`; const elapsedTime = Math.floor((Date.now() - this.startTime) / 1000); if (downloadTime) downloadTime.innerText = `已用时间: ${elapsedTime}秒`; }, async startDownload() { if (this.isDownloading) { this.log('下载任务正在进行中,请等待完成'); return; } if (!this.bid) { this.log('未检测到小说ID,请在小说页面使用此功能'); return; } this.isDownloading = true; this.content = []; this.option = {}; this.downloadedChapters = 0; this.totalChapters = 0; this.startTime = Date.now(); const downloadStatus = document.getElementById('po18-download-status'); if (downloadStatus) downloadStatus.style.display = 'block'; const startBtn = document.getElementById('po18-start'); if (startBtn) { startBtn.disabled = true; startBtn.textContent = '下载中...'; } this.log(`开始下载小说 (BID: ${this.bid}, 格式: ${this.downloadFormat}, 线程数: ${this.threadCount})`); try { await this.downloadNovel(); } catch (err) { this.log(`下载失败: ${err.message || '未知错误'}`); } finally { this.isDownloading = false; if (startBtn) { startBtn.disabled = false; startBtn.textContent = '开始下载'; } } }, async downloadNovel() { // 获取小说详情 this.log('正在获取小说详情...'); const detail = await this.getDetail(this.bid); if (!detail) { this.log('获取小说详情失败'); return; } this.option = Object.assign({}, detail); this.log(`小说信息: ${detail.title} - ${detail.author} (共${detail.pageNum}页)`); // 获取章节列表 this.log('正在获取章节列表...'); const chapters = await this.getChapterList(detail); if (!chapters || chapters.length === 0) { this.log('获取章节列表失败或没有可下载的章节'); return; } this.totalChapters = chapters.length; this.log(`共找到 ${chapters.length} 个可下载章节`); // 下载所有章节内容 this.log('开始下载章节内容...'); const startTime = Date.now(); // 使用滑动窗口并发模式,保持恒定并发数 await this.downloadChaptersWithConcurrency(chapters, this.threadCount); const endTime = Date.now(); const duration = (endTime - startTime) / 1000; this.log(`章节内容下载完成,耗时 ${duration.toFixed(2)} 秒`); // 按顺序排序内容 this.content.sort((a, b) => a.index - b.index); // 生成完整内容 this.log('正在生成最终文件...'); // 整理内容格式 if (this.downloadFormat === 'epub') { // EPUB格式特殊处理 await this.generateEpub(detail, chapters.length, duration); return; } const fileContent = this.formatContent(); // 下载文件 const fileName = `${detail.title}.${this.downloadFormat}`; const fileSize = this.getByteSize(fileContent); const fileSizeText = this.formatFileSize(fileSize); // 使用FileSaver.js保存文件 try { const blob = new Blob([fileContent], { type: this.downloadFormat === 'txt' ? 'text/plain;charset=utf-8' : 'text/html;charset=utf-8' }); window.saveAs(blob, fileName); // WebDAV上传 if (this.webdavConfig.enabled) { const uploaded = await this.uploadToWebDAV(blob, fileName); if (uploaded) { this.log('WebDAV上传成功!'); } } // 记录下载信息 const record = { title: detail.title, author: detail.author, format: this.downloadFormat, size: fileSizeText, time: new Date().toLocaleString(), duration: duration.toFixed(2), chapterCount: chapters.length, cover: detail.cover, tags: detail.tags }; this.downloadRecords.unshift(record); if (this.downloadRecords.length > 50) { this.downloadRecords.pop(); } GM_setValue('downloadRecords', this.downloadRecords); this.log(`下载完成! 文件名: ${fileName}, 大小: ${fileSizeText}, 耗时: ${duration.toFixed(2)}秒`); } catch (e) { this.log(`保存文件失败: ${e.message || '未知错误'}`); } }, async getDetail(bid) { return new Promise((resolve) => { this.log('正在获取小说详情...'); GM_xmlhttpRequest({ method: 'GET', url: `https://www.po18.tw/books/${bid}`, headers: { 'referer': 'https://www.po18.tw', }, onload: (response) => { try { const html = response.responseText; const $ = HTMLParser.parse(html); // 使用自定义的HTML解析替代cheerio let zhText = $.getTextContent("dd.statu"); let zh = zhText.match(/\d+/); // 获取标签 const tags = []; $.querySelectorAll(".book_intro_tags>a").forEach(tag => { tags.push(tag.textContent.trim()); }); // 处理描述 let descContent = $.getTextContent(".B_I_content"); let paragraphs = descContent.split(/\s{2,}/); let desc = paragraphs.map(para => `

${para.trim()}

`).join("\n"); // 构建详情对象 const bookTitle = $.getTextContent("h1.book_name"); const title = bookTitle.split(/(|【|\(/)[0].trim(); const detail = { title: title, author: $.getTextContent("a.book_author"), cover: $.getAttributeValue(".book_cover>img", "src"), description: desc, content: [], tags: tags.join("·"), bid, pub: "po18脸红心跳", pageNum: Math.ceil(zh / 100) || 1 // 确保至少有一页 }; this.log(`获取到小说: ${detail.title} - ${detail.author}`); resolve(detail); } catch (err) { this.log(`解析小说详情失败: ${err.message || '未知错误'}`); resolve(null); } }, onerror: (error) => { this.log(`获取小说详情请求失败: ${error.message || "未知错误"}`); resolve(null); } }); }); }, async getChapterList(detail) { const chapters = []; let globalIndex = 0; for (let page = 1; page <= detail.pageNum; page++) { this.log(`正在获取第${page}/${detail.pageNum} 页章节列表...`); const url = `https://www.po18.tw/books/${detail.bid}/articles?page=${page}`; const pageChapters = await this.getPageChapters(url); if (pageChapters && pageChapters.length > 0) { for (const chapter of pageChapters) { chapter.index = globalIndex++; } chapters.push(...pageChapters); } } return chapters; }, async getPageChapters(url) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'referer': 'https://www.po18.tw', }, onload: (response) => { try { const html = response.responseText; const $ = HTMLParser.parse(html); const chapterItems = []; $.querySelectorAll("#w0>div").forEach((element) => { const chaptNameEl = element.querySelector(".l_chaptname"); if (!chaptNameEl) return; const name = chaptNameEl.textContent.trim(); const isPurchased = !element.textContent.includes('訂購'); if (isPurchased) {const btnLink = element.querySelector(".l_btn>a"); if (!btnLink) return; const href = btnLink.getAttribute("href"); if (!href) return; const id = href.split("/"); if (id.length < 5) return; chapterItems.push({ title: name, bid: id[2], pid: id[4], index: chapterItems.length }); } else { this.log(`章节 "${name}" 需要购买,已跳过`); } }); resolve(chapterItems); } catch (err) { this.log(`解析章节列表失败: ${err.message || '未知错误'}`); resolve([]); } }, onerror: (error) => { this.log(`获取章节列表请求失败: ${error.message || "未知错误"}`); resolve([]); } }); }); }, // 滑动窗口并发下载 async downloadChaptersWithConcurrency(chapters, concurrency) { let index = 0; const total = chapters.length; const results = []; const worker = async () => { while (index < total) { const currentIndex = index++; const chapter = chapters[currentIndex]; await this.getChapterContent(chapter); } }; // 启动多个并发worker const workers = []; for (let i = 0; i < Math.min(concurrency, total); i++) { workers.push(worker()); } await Promise.all(workers); }, async getChapterContent(chapter) { return new Promise((resolve) => { const { bid, pid, index, title } = chapter; GM_xmlhttpRequest({ method: 'GET', url: `https://www.po18.tw/books/${bid}/articlescontent/${pid}`, headers: { 'referer': `https://www.po18.tw/books/${bid}/articles/${pid}`, 'x-requested-with': 'XMLHttpRequest' }, onload: (response) => { try { let content = response.responseText.replace(/   /g, ""); const $ = HTMLParser.parse(content); // 移除引用块和h1标签 $.remove("blockquote"); $.remove("h1"); // 获取标题(在移除前获取) const tempDoc = HTMLParser.parse(response.responseText); let name = tempDoc.getTextContent("h1"); // 将章节内容存储到数组 this.content[index] = { title: name || title, data: $.getHTML().replace(/ /g, ""), rawText: $.getText(), index: index }; this.log(`已下载章节: ${name || title}`); this.downloadedChapters++; this.updateProgress(this.downloadedChapters, this.totalChapters); resolve(); } catch (err) { this.log(`下载章节 "${title}" 失败: ${err.message || '未知错误'}`); resolve(); } }, onerror: (error) => { this.log(`下载章节 "${title}" 请求失败: ${error.message || "未知错误"}`); resolve(); } }); }); }, // 增强的内容格式化方法 formatContent() { if (this.downloadFormat === 'txt') { // TXT格式增强,加入简介和标签 let content = `${this.option.title}\n作者: ${this.option.author}\n\n`; // 加入标签 if (this.option.tags) { content += `标签: ${this.option.tags}\n\n`; } // 加入简介 if (this.option.description) { const description = this.option.description.replace(/<[^>]+>/g, ''); // 移除HTML标签 content += `【简介】\n${description}\n\n`; } // 加入正文内容 content += `【正文】\n`; this.content.forEach(chapter => { if (chapter) { content += '\n\n' + chapter.title + '\n\n'; content += chapter.rawText.replace(/\s+/g, '\n\n'); } }); return content; } else if (this.downloadFormat === 'epub') { // EPUB格式 - 返回null,由generateEpub处理 return null; } else { // HTML格式 - 增强为阅读器风格 // 创建一个精美的HTML电子书阅读界面 let content = ` ${this.option.title} - ${this.option.author}
${this.option.title}封面

${this.option.title}

作者:${this.option.author}

${this.option.tags ? this.option.tags.split('·').map(tag => `${tag.trim()}`).join('') : ''}
${this.option.description}
`; // 添加章节页面 this.content.forEach((chapter, index) => { if (chapter) { content += `

${chapter.title}

${chapter.data}
`; } }); // 添加导航按钮和侧边栏 content += `

阅读设置

背景颜色

字体大小

特大

行间距

紧凑
适中
宽松
超宽

字间距

正常
略宽
宽松
超宽

字体选择

黑体
宋体
楷体
行书
`; return content; } }, // EPUB生成方法 async generateEpub(detail, chapterCount, duration) { this.log('正在生成EPUB文件...'); const saveAsFunc = _saveAs; try { const zip = new _JSZip(); const bookId = 'po18-' + detail.bid + '-' + Date.now(); this.log('正在构建EPUB结构...'); // 1. mimetype文件(必须是第一个文件,不压缩) zip.file('mimetype', 'application/epub+zip', { compression: 'STORE' }); // 2. META-INF/container.xml zip.file('META-INF/container.xml', ` `); // 3. OEBPS/content.opf let manifest = ''; let spine = ''; // 添加封面页 manifest += ' \n'; spine += ' \n'; // 添加章节 this.content.forEach((chapter, index) => { if (chapter) { manifest += ` \n`; spine += ` \n`; } }); // 添加目录和样式 manifest += ' \n'; manifest += ' \n'; manifest += ' \n'; const contentOpf = ` ${bookId} ${this.escapeXml(detail.title)} ${this.escapeXml(detail.author)} zh-TW PO18脸红心跳 ${new Date().toISOString().replace(/\.\d+Z$/, 'Z')} ${manifest} ${spine} `; zip.file('OEBPS/content.opf', contentOpf); // 4. 样式文件 - 完整的main.css const mainCss = `/* EPUB主样式表 */ @charset "utf-8"; @import url("fonts.css"); /* ==================== 基础样式 ==================== */ body { margin: 0; padding: 0; text-align: justify; font-family: "DK-SONGTI", "Songti SC", "st", "宋体", "SimSun", "STSong", serif; color: #333333; } p { margin-left: 0; margin-right: 0; line-height: 1.3em; text-align: justify; text-justify: inter-ideograph; text-indent: 2em; duokan-text-indent: 2em; } div { margin: 0; padding: 0; line-height: 130%; text-align: justify; } /* ==================== 封面图片 ==================== */ div.top-img-box { text-align: center; duokan-bleed: lefttopright; } img.top-img { width: 100%; } /* ==================== 分卷标题 ==================== */ h1.part-title { width: 1em; margin: 10% auto auto auto; font-family: "SourceHanSerifSC-Bold"; font-size: 1.3em; text-align: center; color: #a80000; padding: 0.2em; border: 2px solid #a80000; } /* ==================== 章节标题 ==================== */ h2.chapter-title { margin: 0 12% 2em 12%; padding: 0 4px 0 4px; line-height: 1.3em; font-family: "SourceHanSerifSC-Bold"; text-align: center; font-size: 1em; color: #a80000; } span.chapter-sequence-number { font-family: "FZLanTYKXian"; font-size: x-small; color: #676767; } span.sub-heading { font-size: small; } /* ==================== 简介标题 ==================== */ h2.introduction-title, h3.introduction-title { margin: 2em auto 2em auto; font-family: "SourceHanSerifSC-Bold"; text-align: center; font-size: 1em; color: #a80000; padding: 0; } /* ==================== 特殊段落样式 ==================== */ p.kt { font-family: "STKaiti"; } p.text-right { text-align: right; text-indent: 0em; duokan-text-indent: 0em; } p.end { margin: 2em auto auto auto; text-align: center; font-family: "FZLanTYKXian"; font-size: small; color: #a80000; text-indent: 0em; duokan-text-indent: 0em; } /* ==================== 设计信息框 ==================== */ div.design-box { margin: 20% 2% auto 2%; padding: 0.8em; border: 2px solid rgba(246, 246, 246, 0.3); border-radius: 7px; background-color: rgba(246, 246, 246, 0.3); } h1.design-title { margin: 1em auto 1em auto; padding: 0 4px 0 4px; font-family: "FZLanTYKXian"; font-size: 65%; color: #808080; text-align: center; } p.design-content { margin-top: 1em; font-family: "FZLanTYKXian"; font-size: 60%; color: #808080; text-indent: 0em; duokan-text-indent: 0em; } span.duokanicon { font-family: "Asheng"; color: #EC902E; } hr.design-line { border-style: dashed; border-width: 1px 00 0; border-color: rgba(200, 200, 193, 0.15); } /* ==================== 书籍简介样式 ==================== */ .book_intro, .book-intro { max-width: 100%; margin: 0 auto; padding: 1em; } .book_intro h3, .book-intro h3 { margin: 0 0 1.5em 0; padding-bottom: 0.5em; font-family: "SourceHanSerifSC-Bold"; font-size: 1.2em; text-align: center; color: #a80000; border-bottom: 2px solid #a80000; } .B_I_content, .intro-content { line-height: 1.8; color: #333333; font-size: 1em; } .B_I_content p, .intro-content p { margin: 0.8em 0; line-height: 1.8; text-indent: 2em; duokan-text-indent: 2em; } /* ==================== 简介特殊段落 ==================== */ .tagline { font-style: italic; color: #7f8c8d; text-align: center; margin: 1.5em 0; text-indent: 0 !important; duokan-text-indent: 0 !important; } .meta-info { text-align: center; font-weight: bold; color: #34495e; margin: 1em 0; text-indent: 0 !important; duokan-text-indent: 0 !important; } /* ==================== 文字颜色样式 ==================== */ .text-red, .color-red { color: #e74c3c; } .text-orange, .color-orange { color: #e67e22; } .text-gray, .color-gray { color: #999999; } .text-green, .color-green { color: #27ae60; } .text-black, .color-black { color: #000000; } .color-dark-red { color: #c0392b; } /* ==================== 文字大小样式 ==================== */ .text-medium, .font-size-16 { font-size: 16px; } .text-large, .font-size-22 { font-size: 22px; } .text-xlarge, .font-size-20 { font-size: 20px; } .font-size-12 { font-size: 12px; } .font-size-18 { font-size: 18px; } /* ==================== 警告样式 ==================== */ .warning-primary { background: #ffe6e6; border-left: 4px solid #e74c3c; padding: 0.8em 1em; margin: 1em 0; font-weight: bold; color: #e74c3c;text-indent: 0 !important; duokan-text-indent: 0 !important; } .warning-highlight { background: #fff3cd; border: 2px solid #e67e22; padding: 1em; margin: 1.5em 0; font-size: 1.3em; font-weight: bold; color: #e67e22; text-align: center; text-indent: 0 !important; duokan-text-indent: 0 !important;border-radius: 5px; } /* ==================== 内容警告区块 ==================== */ .content-warning { background: #fff5f5; border: 2px solid #e74c3c; border-radius: 6px; padding: 1.2em; margin: 1.5em 0; } .warning-title { font-size: 1.2em; font-weight: bold; color: #e74c3c; margin: 0 0 0.8em 0; text-indent: 0 !important; duokan-text-indent: 0 !important; } .warning-action { font-weight: bold; color: #c0392b; text-indent: 0 !important; duokan-text-indent: 0 !important; } .content-warning p { margin: 0.8em 0; text-indent: 2em; duokan-text-indent: 2em; } .content-warning strong { color: #e74c3c;font-size: 1.1em; } /* ==================== 备注样式 ==================== */ .note { color: #7f8c8d; font-size: 0.95em; text-indent: 0 !important; duokan-text-indent: 0 !important;padding-left: 1em; } /* ==================== 间距控制 ==================== */ .spacing { height: 10px; margin: 0; } /* ==================== 标签样式 ==================== */ .book_intro_tags, .book-tags { margin-top: 1.5em; padding-top: 1em; border-top: 1px solid #dddddd; display: flex; flex-wrap: wrap; gap: 0.5em; } /* ==================== 标签样式 ==================== */ .tag { display: inline-block; padding: 0.4em 2em; background: #FFB3D9; /* 🎀 改为粉色 */ color: #ffffff; border-radius: 15px; font-size: 0.85em; text-decoration: none; font-weight: 500; text-indent: 0; duokan-text-indent: 0; } /* Kindle/Mobi 适配 */ @media amzn-kf8, amzn-mobi { .tag { border: 1px solid #FFB3D9; /* 边框也改为粉色 */ } } /* 夜间模式 */ @media (prefers-color-scheme: dark) { .tag { background: #D85A8C; /* 夜间模式粉色 */ color: #e0e0e0; } } /* ==================== 更新信息框 ==================== */ .update-info { background: linear-gradient(to right, #fff5f5, #ffffff); border-left: 5px solid #c0392b; padding: 0.8em 1em; margin: 1em 0; border-radius: 0 5px 5px 0; } .update-info p { margin: 0.5em 0; } /* ==================== 强调样式 ==================== */ strong { font-weight: bold; } em { font-style: italic; } /* ==================== 通用工具类 ==================== */ .text-center { text-align: center; text-indent: 0 !important; duokan-text-indent: 0 !important; } .text-left { text-align: left; } .no-indent { text-indent: 0 !important; duokan-text-indent: 0 !important; } /* ==================== 响应式设计 ==================== */ @media screen and (max-width: 600px) { .book_intro, .book-intro { padding: 0.8em; } .text-large, .font-size-22 { font-size: 20px; } .text-xlarge, .font-size-20 { font-size: 18px; } .font-size-18 { font-size: 16px; } } /* ==================== Kindle/Mobi 适配 ==================== */ @media amzn-kf8, amzn-mobi { .book_intro, .book-intro { background: transparent; } .warning-primary, .warning-highlight, .content-warning { background: transparent; } .tag { border: 1px solid #667eea; } } /* ==================== 夜间模式支持 ==================== */ @media (prefers-color-scheme: dark) { body { background: #1a1a1a; color: #e0e0e0; } .book_intro, .book-intro { background: #2a2a2a; } .book_intro h3, .book-intro h3, h2.introduction-title { color: #f39c12; border-bottom-color: #f39c12; } .B_I_content, .intro-content { color: #d0d0d0; } .warning-primary { background: #3d1f1f; color: #ff7675; border-left-color: #ff7675; } .warning-highlight { background: #3d3520; border-color: #f39c12; color: #f39c12; } .content-warning { background: #3d1f1f; border-color: #ff7675;} .warning-title, .warning-action { color: #ff7675; } .tag { background: #4a5568; color: #e0e0e0; } }`; zip.file('OEBPS/Styles/main.css', mainCss); // 5. 简介页/封面页 const tagsHtml = detail.tags ? detail.tags.split('·').map(t => `${this.escapeXml(t.trim())}`).join('') : ''; // 处理描述,转换为p标签 let descParagraphs = ''; if (detail.description) { const descText = detail.description.replace(/<\/?p>/gi, '').replace(//gi, '\n'); descParagraphs = descText.split(/\n+/).filter(p => p.trim()).map(p => `

${this.escapeXml(p.trim())}

`).join('\n'); } const coverXhtml = ` 内容简介

内容简介

${tagsHtml}

书名:${this.escapeXml(detail.title)}

作者:${this.escapeXml(detail.author)}

${descParagraphs}

本书采用PO18小说下载器自动生成,仅供个人学习之用。


`; zip.file('OEBPS/cover.xhtml', coverXhtml); // 6. 章节文件 this.content.forEach((chapter, index) => { if (chapter) { // 解析章节标题,分离序号和名称 const titleMatch = chapter.title.match(/^(第[\u4e00-\u9fa5\d]+章)\s*(.*)$/); let seqNum = ''; let chapterName = chapter.title; if (titleMatch) { seqNum = titleMatch[1]; chapterName = titleMatch[2] || ''; } // 处理正文内容,转换为p标签 let contentHtml = ''; const rawContent = chapter.data || chapter.rawText || ''; const textContent = rawContent .replace(//gi, '\n') .replace(/<\/p>\s*

/gi, '\n') .replace(/<\/?p>/gi, '') .replace(/ /g, ' '); contentHtml = textContent.split(/\n+/).filter(p => p.trim()).map(p => `

${p.trim()}

`).join('\n'); const chapterXhtml = ` ${this.escapeXml(chapter.title)}

${seqNum ? `${this.escapeXml(seqNum)}
` : ''}${this.escapeXml(chapterName || chapter.title)}

${contentHtml} `; zip.file(`OEBPS/chapter${index}.xhtml`, chapterXhtml); } }); // 7. 目录文件 toc.xhtml (EPUB3 nav) let tocItems = '
  • 内容简介
  • \n'; this.content.forEach((chapter, index) => { if (chapter) { tocItems += `
  • ${this.escapeXml(chapter.title)}
  • \n`; } }); const tocXhtml = ` 目录 `; zip.file('OEBPS/toc.xhtml', tocXhtml); // 8. NCX文件 (EPUB2兼容) let ncxNavPoints = ` 内容简介 \n`; let playOrder = 2; this.content.forEach((chapter, index) => { if (chapter) { ncxNavPoints += ` ${this.escapeXml(chapter.title)} \n`; } }); const ncx = ` ${this.escapeXml(detail.title)} ${ncxNavPoints} `; zip.file('OEBPS/toc.ncx', ncx); // 生成并下载 this.log('正在压缩EPUB文件...'); const self = this; try { const zipPromise = zip.generateAsync({ type: 'blob', mimeType: 'application/epub+zip' }); zipPromise.then(function(blob) { self.log('EPUB压缩完成,大小: ' + self.formatFileSize(blob.size)); const fileName = detail.title.replace(/[\\/:*?"<>|]/g, '_') + '.epub'; // 使用saveAs或备用方法下载 if (saveAsFunc) { saveAsFunc(blob, fileName); self.log('正在触发下载...'); } else { // 备用下载方法 self.log('使用备用下载方法...'); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); setTimeout(function() { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); } // WebDAV上传 if (self.webdavConfig.enabled) { self.uploadToWebDAV(blob, fileName).then(function(uploaded) { if (uploaded) { self.log('EPUB已上传到WebDAV!'); } }); } const fileSizeText = self.formatFileSize(blob.size); // 记录下载信息 const record = { title: detail.title, author: detail.author, format: 'epub', size: fileSizeText, time: new Date().toLocaleString(), duration: duration.toFixed(2), chapterCount: chapterCount, cover: detail.cover, tags: detail.tags }; self.downloadRecords.unshift(record); if (self.downloadRecords.length > 50) self.downloadRecords.pop(); GM_setValue('downloadRecords', self.downloadRecords); self.log('EPUB下载完成! 文件名: ' + fileName + ', 大小: ' + fileSizeText); }).catch(function(e) { self.log('生成EPUB失败: ' + (e.message || '未知错误')); console.error('EPUB生成错误:', e); }); } catch (syncErr) { this.log('压缩调用失败: ' + (syncErr.message || '未知错误')); console.error('压缩同步错误:', syncErr); } } catch (err) { this.log(`EPUB生成过程出错: ${err.message || '未知错误'}`); } }, // XML转义 escapeXml(str) { if (!str) return ''; return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); }, getByteSize(string) { return new Blob([string]).size; }, formatFileSize(bytes) { if (bytes < 1024) { return bytes + ' B'; } else if (bytes < 1024 * 1024) { return (bytes / 1024).toFixed(2) + ' KB'; } else { return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; } }, renderDownloadRecords() { const container = document.getElementById('po18-records-container'); if (!container) { return; } if (this.downloadRecords.length === 0) { container.innerHTML = '
    暂无下载记录
    '; return; } let html = ''; this.downloadRecords.forEach((record) => { // 添加封面显示 const coverHtml = record.cover ? `${record.title}封面` : ''; // 添加标签显示 let tagsHtml = ''; if (record.tags) { const tagsList = record.tags.split('·'); tagsHtml = '
    '; tagsList.forEach(tag => { if (tag.trim()) { tagsHtml += `${tag.trim()} `; } }); tagsHtml += '
    '; } html += `
    ${coverHtml}

    ${record.title || "未知标题"}

    作者: ${record.author || "未知作者"} 格式: ${record.format ? record.format.toUpperCase() : "未知格式"}
    大小: ${record.size || "未知大小"} 章节数: ${record.chapterCount || "未知"}
    时间: ${record.time || "未知时间"} 耗时: ${record.duration || "0"}秒
    ${tagsHtml}
    `; }); container.innerHTML = html; } }; // 初始化下载器 Po18Downloader.init(); })();