您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
下载PO18小说,支持TXT或HTML格式,多线程下载,记录下载历史,增强阅读体验,查看已购书架
// ==UserScript== // @name PO18小说下载器 // @namespace http://tampermonkey.net/ // @version 1.3.4 // @description 下载PO18小说,支持TXT或HTML格式,多线程下载,记录下载历史,增强阅读体验,查看已购书架 // @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 // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js // @require https://code.jquery.com/ui/1.12.1/jquery-ui.min.js // ==/UserScript== (function() { 'use strict'; // ==== 辅助函数:HTML解析器(替代Cheerio) ==== const HTMLParser = { parse: function(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); return { querySelector: function(selector) { return doc.querySelector(selector); }, querySelectorAll: function(selector) { return Array.from(doc.querySelectorAll(selector)); }, getTextContent: function(selector) { const el = doc.querySelector(selector); return el ? el.textContent.trim() : ''; }, getAttributeValue: function(selector, attribute) { const el = doc.querySelector(selector); return el ? el.getAttribute(attribute) : null; }, getText: function() { return doc.body.textContent; }, getHTML: function() { return doc.body.innerHTML; }, remove: function(selector) { doc.querySelectorAll(selector).forEach(el => el.remove()); return this; } }; } }; // ==== 样式设置 - 修改为淡粉色主题 ==== 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; } `); // ==== 主要功能实现 ==== 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, init() { this.createUI(); this.bindEvents(); this.loadSettings(); this.detectNovelPage(); // 检查登录状态 this.checkLoginStatus(); }, createUI() { // 创建悬浮按钮 const floatButton = document.createElement('div'); floatButton.className = 'po18-float-button'; floatButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" fill="white"><path d="M12 21q-3.75 0-6.375-2.625T3 12Q3 8.25 5.625 5.625T12 3t6.375 2.625T21 12t-2.625 6.375T12 21Z"/></svg>'; document.body.appendChild(floatButton); // 创建主面板 const panel = document.createElement('div'); panel.className = 'po18-panel'; // 使用模板字符串确保HTML格式正确 panel.innerHTML = ` <div class="po18-header po18-draggable"> <span>PO18小说下载器</span> <span style="cursor:pointer" id="po18-close">✕</span> </div> <div class="po18-tabs"> <div class="po18-tab active" data-tab="download">下载</div> <div class="po18-tab" data-tab="log">日志</div> <div class="po18-tab" data-tab="records">记录</div> <div id="po18-bookshelf-tab" class="po18-tab" data-tab="bookshelf" style="display:none">书架</div> <div class="po18-tab" data-tab="about">关于</div> </div> <div class="po18-tab-content"> <div class="po18-tab-pane active" id="po18-tab-download"> <div id="po18-book-details-container" class="po18-card"></div> <div class="po18-card"> <div class="po18-form-group"> <label for="po18-format">下载格式</label> <select id="po18-format" class="po18-select"> <option value="txt">TXT</option> <option value="html">HTML</option> </select> </div> <div class="po18-form-group"> <label for="po18-thread">线程数</label> <select id="po18-thread" class="po18-select"> <option value="1">1线程</option> <option value="3" selected>3线程</option> <option value="5">5线程</option> <option value="10">10线程</option> </select> </div> <button id="po18-start" class="po18-button">开始下载</button> </div> <div id="po18-download-status" style="display:none"> <div class="po18-card"> <h4>下载进度</h4> <div class="po18-progress"> <div class="po18-progress-bar" id="po18-progress"></div> </div> <div id="po18-progress-text">0/0 章节 (0%)</div> <div id="po18-download-time">已用时间: 0秒</div> </div> </div> </div> <div class="po18-tab-pane" id="po18-tab-log"> <div class="po18-card"> <div class="po18-log" id="po18-logs"></div> </div> </div> <div class="po18-tab-pane" id="po18-tab-records"> <div id="po18-records-container"></div> </div> <div class="po18-tab-pane" id="po18-tab-bookshelf"> <div class="po18-card"> <div class="po18-bookshelf-header"> <h3>我的书架</h3> <button id="po18-refresh-bookshelf" class="po18-button" style="padding: 5px 10px; font-size: 12px;">刷新</button> </div><div id="po18-bookshelf-status">加载中...</div> <div id="po18-bookshelf-container"></div> </div> </div> <div class="po18-tab-pane" id="po18-tab-about"> <div class="po18-card"> <h3>PO18小说下载器增强版 v1.3.4</h3><p>这是一款用于下载PO18网站小说的工具,支持TXT和HTML格式下载,多线程下载等功能。</p> <p>作者github:wenmoux:</p> <p>新增功能:</p> <ol> <li>全新的粉色主题界面</li> <li>显示小说封面、作者和标签</li> <li>增强HTML输出,支持电子书式的左右翻页</li> <li>阅读界面支持字体大小、颜色主题调整</li> <li>新增行间距、字间距调整功能</li> <li>优化正文排版和阅读舒适度</li> <li>新增书架功能,便于管理已购买小说</li> </ol> <p>使用方法:</p> <ol> <li>在小说页面点击悬浮按钮</li> <li>选择下载格式和线程数</li> <li>点击"开始下载"按钮</li> </ol> <p>注意:需要先登录PO18网站才能下载已购买的章节。</p> </div> </div> </div> `; 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')); }, 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 = '<div class="po18-empty-message">没有找到已购书籍,请确认已登录PO18网站</div>'; } 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 += `<span class="po18-book-tag">${tag.trim()}</span>`; } }); } html += ` <div class="po18-book-item"> <div class="po18-book-info"> <img class="po18-book-cover" src="${coverUrl}" alt="${book.title}封面"> <div class="po18-book-details"> <h3 class="po18-book-title">${book.title}</h3> <div class="po18-book-author">作者: ${book.author}</div> <div class="po18-book-tags">${tagsHTML}</div> <div class="po18-book-year">购买年份: ${book.year}</div> </div> </div> <div class="po18-book-actions"> <a href="${book.detail}" target="_blank" class="po18-button po18-button-small">查看</a> <button class="po18-button po18-button-small po18-download-book" data-bid="${book.bid}" data-title="${book.title}">下载</button> </div> </div> `; }); } 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 += `<span class="po18-book-tag">${tag.trim()}</span>`; } }); } // 构造小说详情HTML const html = ` <div class="po18-book-info"> <img class="po18-book-cover" src="${detail.cover}" alt="${detail.title}封面"> <div class="po18-book-details"> <h3 class="po18-book-title">${detail.title}</h3> <div class="po18-book-author">作者: ${detail.author}</div> <div class="po18-book-tags"> ${tagsHTML} </div> </div> </div> `; 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(); // 根据线程数分配任务 const batchSize = this.threadCount; for (let i = 0; i < chapters.length; i += batchSize) { const batch = chapters.slice(i, i + batchSize); await Promise.all(batch.map(chapter => this.getChapterContent(chapter))); // 更新进度 this.updateProgress(Math.min(i + batchSize, chapters.length), chapters.length); } 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('正在生成最终文件...'); // 整理内容格式 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); // 记录下载信息 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 => `<p>${para.trim()}</p>`).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 = []; 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) { 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 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); // 移除引用块 $.remove("blockquote"); // 获取标题和内容 let name = $.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 { // HTML格式 - 增强为阅读器风格 // 创建一个精美的HTML电子书阅读界面 let content = `<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>${this.option.title} - ${this.option.author}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> :root { --bg-color: #FFF0F3; --text-color: #333; --theme-color: #FF8BA7; --link-color: #D46A87; --font-size: 18px; --font-family: 'Noto Sans SC', sans-serif; --line-height: 1.8; --letter-spacing: normal; } * { box-sizing: border-box;margin: 0; padding: 0; } body { font-family: var(--font-family); font-size: var(--font-size); background-color: var(--bg-color); color: var(--text-color); line-height: var(--line-height); letter-spacing: var(--letter-spacing); overflow: hidden; height: 100vh; }/* 书本结构 */ .book-container { display: flex; height: 100vh; width: 100%; position: relative; overflow: hidden; }.book-content { flex: 1; height: 100%; overflow: hidden; position: relative; } /* 页面结构 */ .page { height: 100%; width: 100%; display: none; padding: 40px 60px; /* 更宽松的页面边距 */ overflow-y: auto; position: absolute; left: 0; top: 0; transition: transform 0.5s ease; background-color: white; box-shadow: 0 0 10px rgba(0,0,0,0.1); scroll-behavior: smooth; /* 优雅的滚动效果 */ } .page.active { display: block; z-index: 2; }/* 封面页样式 */ .cover-page { text-align: center; padding: 40px; display: flex; flex-direction: column; justify-content: center; align-items: center; } .cover-image { max-width: 50%; max-height: 60vh; box-shadow: 0 5px 15px rgba(0,0,0,0.2); margin-bottom: 30px; border-radius: 5px; } .book-title { font-size: 2.5em; margin-bottom: 10px; color: var(--theme-color); } .book-author { font-size: 1.5em; margin-bottom: 20px; font-weight: normal; color: #666; } .book-tags { margin: 20px 0; display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; } .book-tag { background-color: var(--theme-color); color: white; padding: 5px 12px; border-radius: 15px; font-size: 0.9em; } .book-description { max-width: 800px; margin: 0 auto; text-align: left; } .book-description p { margin-bottom: 10px; } /* 章节样式优化 */ .chapter { padding-bottom: 30px; letter-spacing: var(--letter-spacing); line-height: var(--line-height); } .chapter-content { max-width: 800px; margin: 0 auto; } .chapter-title { font-weight: 500; padding-bottom: 25px; margin-bottom: 30px; border-bottom: 1px solid rgba(0,0,0,0.1); text-align: center; color: var(--theme-color); } .chapter p { margin-bottom: 1.2em; text-indent: 2em; /* 段落首行缩进 */ } /* 改善段落间距的一致性 */ .chapter p + p { margin-top: 0.5em; } /* 导航控制 */ .nav-buttons { position: fixed; bottom: 20px; left: 0; right: 0; display: flex; justify-content: center; gap: 20px; z-index: 100; opacity: 0.7; transition: opacity 0.3s; } .nav-buttons:hover { opacity: 1; } .nav-btn { background-color: var(--theme-color); color: white; border: none; padding: 10px 20px; border-radius: 30px; cursor: pointer; box-shadow: 0 3px 5px rgba(0,0,0,0.2); font-size: 0.9em; transition: all 0.2s; } .nav-btn:hover { transform: translateY(-2px); box-shadow: 0 5px 8px rgba(0,0,0,0.2); } .nav-btn:active { transform: translateY(1px); box-shadow: 0 2px 3px rgba(0,0,0,0.2); } /* 目录按钮和侧边栏 */ .toc-toggle { position: fixed; top: 20px; right: 20px; z-index: 101; padding: 10px; background-color: var(--theme-color); color: white; border-radius: 50%; cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } .sidebar { position: fixed; top: 0; right: -300px; width: 300px; height: 100vh; background-color: white; box-shadow: -5px 0 10px rgba(0,0,0,0.1); overflow-y: auto; z-index: 100; transition: right 0.3s ease-in-out; padding: 20px; } .sidebar.active { right: 0; } .close-sidebar { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 1.5em; cursor: pointer; color: #666; } .toc-title { margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #eee; text-align: center; color: var(--theme-color); } .toc-item { padding: 10px; cursor: pointer; transition: all 0.2s; border-radius: 5px; } .toc-item:hover { background-color: #f5f5f5; color: var(--theme-color); } .toc-item.active { background-color: var(--theme-color); color: white; } /* 设置面板 */ .settings-container { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 80%; max-width: 500px; background-color: white; border-radius: 10px; box-shadow: 0 5px 25px rgba(0,0,0,0.2); padding: 20px; z-index: 200; } .settings-container.active { display: block; } .settings-title { margin-bottom: 20px; text-align: center; color: var(--theme-color); font-size: 1.5em; } .settings-section { margin-bottom: 20px; } .settings-section-title { margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px solid #eee; font-size: 1.1em; color: #666; } .color-options, .font-size-options, .font-family-options, .spacing-options { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 15px; } .color-option { width: 30px; height: 30px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; } .color-option.active { border-color: var(--theme-color); } .font-option { padding: 5px 10px; border: 1px solid #ddd; border-radius: 5px; cursor: pointer; } .font-option.active { background-color: var(--theme-color); color: white; border-color: var(--theme-color); } .close-settings { position: absolute; top: 10px; right: 10px; background: none; border: none; font-size: 1.5em; cursor: pointer; color: #666; } .overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); z-index: 199; } .overlay.active { display: block; } /* 悬浮控制栏 */ .float-controls { position: fixed; bottom: 20px; left: 20px; display: flex; gap: 10px; z-index: 100; } .float-btn { background-color: var(--theme-color); color: white; border: none; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 3px 5px rgba(0,0,0,0.2); transition: all 0.2s; } .float-btn:hover { transform: scale(1.1); } /* 适应深色模式的文本调整 */ @media (prefers-color-scheme: dark) { .color-option[data-color="#212121"][data-text="#FFFFFF"] { border-color: rgba(255,255,255,0.5); } } /* 响应式设计 */ @media (max-width: 768px) { .page { padding: 20px; } .cover-image { max-width: 70%; } .book-title { font-size: 2em; } .float-controls { flex-direction: column; } .nav-buttons { bottom: 10px; } .nav-btn { padding: 8px 15px; font-size: 0.8em; } } </style> </head> <body> <div class="book-container"> <div class="book-content" id="bookContent"> <!-- 封面页 --> <div class="page cover-page active" data-page="cover"> <img class="cover-image" src="${this.option.cover}" alt="${this.option.title}封面"> <h1 class="book-title">${this.option.title}</h1> <h2 class="book-author">作者:${this.option.author}</h2> <div class="book-tags"> ${this.option.tags ? this.option.tags.split('·').map(tag => `<span class="book-tag">${tag.trim()}</span>`).join('') : ''} </div> <div class="book-description"> ${this.option.description} </div> </div>`; // 添加章节页面 this.content.forEach((chapter, index) => { if (chapter) { content += ` <!-- 章节 ${index + 1} --> <div class="page" data-page="${index + 1}"> <div class="chapter"> <h2 class="chapter-title">${chapter.title}</h2> <div class="chapter-content"> ${chapter.data} </div> </div> </div>`; } }); // 添加导航按钮和侧边栏 content += ` </div> </div> <!-- 导航按钮 --> <div class="nav-buttons"> <button class="nav-btn" id="prevBtn">上一页</button> <button class="nav-btn" id="nextBtn">下一页</button> </div> <!-- 目录按钮 --> <div class="toc-toggle" id="tocToggle"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <line x1="3" y1="12" x2="21" y2="12"></line> <line x1="3" y1="6" x2="21" y2="6"></line> <line x1="3" y1="18" x2="21" y2="18"></line> </svg> </div> <!-- 侧边栏目录 --> <div class="sidebar" id="sidebar"> <button class="close-sidebar" id="closeSidebar">×</button> <h3 class="toc-title">目录</h3> <div id="tocContainer"> <div class="toc-item" data-page="cover">封面</div>`; // 添加章节目录 this.content.forEach((chapter, index) => { if (chapter) { content += ` <div class="toc-item" data-page="${index + 1}">${chapter.title} <span style="float:right; font-size:0.8em; color:#999;">${index + 1}/${this.content.length}</span></div>`; } }); content += ` </div> </div> <!-- 悬浮控制栏 --> <div class="float-controls"> <div class="float-btn" id="settingsBtn" title="设置"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="3"></circle> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> </svg> </div> </div> <!-- 设置面板 --> <div class="overlay" id="overlay"></div> <div class="settings-container" id="settingsPanel"> <button class="close-settings" id="closeSettings">×</button> <h3 class="settings-title">阅读设置</h3> <div class="settings-section"> <h4 class="settings-section-title">背景颜色</h4> <div class="color-options"> <div class="color-option active" style="background-color: #FFF0F3;" data-color="#FFF0F3"></div> <div class="color-option" style="background-color: #FFFFFF;" data-color="#FFFFFF"></div> <div class="color-option" style="background-color: #E8F5E9;" data-color="#E8F5E9"></div> <div class="color-option" style="background-color: #FFF8E1;" data-color="#FFF8E1"></div> <div class="color-option" style="background-color: #E3F2FD;" data-color="#E3F2FD"></div> <div class="color-option" style="background-color: #212121;" data-color="#212121" data-text="#FFFFFF"></div> </div> </div> <div class="settings-section"> <h4 class="settings-section-title">字体大小</h4> <div class="font-size-options"> <div class="font-option" data-size="14px">小</div> <div class="font-option active" data-size="18px">中</div> <div class="font-option" data-size="22px">大</div> <div class="font-option" data-size="26px">特大</div> </div> </div> <div class="settings-section"> <h4 class="settings-section-title">行间距</h4> <div class="spacing-options"> <div class="font-option" data-lineheight="1.5">紧凑</div> <div class="font-option active" data-lineheight="1.8">适中</div> <div class="font-option" data-lineheight="2.2">宽松</div> <div class="font-option" data-lineheight="2.6">超宽</div> </div> </div> <div class="settings-section"> <h4 class="settings-section-title">字间距</h4> <div class="spacing-options"> <div class="font-option active" data-letterspacing="normal">正常</div> <div class="font-option" data-letterspacing="0.05em">略宽</div> <div class="font-option" data-letterspacing="0.1em">宽松</div> <div class="font-option" data-letterspacing="0.15em">超宽</div> </div> </div> <div class="settings-section"> <h4 class="settings-section-title">字体选择</h4> <div class="font-family-options"> <div class="font-option active" data-font="'Noto Sans SC', sans-serif">黑体</div> <div class="font-option" data-font="'Noto Serif SC', serif">宋体</div> <div class="font-option" data-font="'LXGW WenKai', cursive">楷体</div> <div class="font-option" data-font="'Long Cang', cursive">行书</div></div> </div></div><script> document.addEventListener('DOMContentLoaded', function() { // 页面管理 let currentPage = 'cover'; const totalPages = ${this.content.length + 1}; // +1 for cover page // 获取元素 const bookContent = document.getElementById('bookContent'); const pages = document.querySelectorAll('.page'); const prevBtn = document.getElementById('prevBtn'); const nextBtn = document.getElementById('nextBtn'); const tocToggle = document.getElementById('tocToggle'); const sidebar = document.getElementById('sidebar'); const closeSidebar = document.getElementById('closeSidebar'); const tocContainer = document.getElementById('tocContainer'); const settingsBtn = document.getElementById('settingsBtn'); const settingsPanel = document.getElementById('settingsPanel'); const closeSettings = document.getElementById('closeSettings'); const overlay = document.getElementById('overlay'); // 初始化首页 updatePageButtons(); // 导航按钮事件 prevBtn.addEventListener('click', () => { navigateToPage(getPreviousPage()); }); nextBtn.addEventListener('click', () => { navigateToPage(getNextPage()); }); // 键盘导航 document.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { navigateToPage(getPreviousPage()); } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { navigateToPage(getNextPage()); } }); // 目录事件 tocToggle.addEventListener('click', () => { sidebar.classList.add('active'); }); closeSidebar.addEventListener('click', () => { sidebar.classList.remove('active'); }); // 目录项点击事件 document.querySelectorAll('.toc-item').forEach(item => { item.addEventListener('click', () => { const page = item.dataset.page; navigateToPage(page); sidebar.classList.remove('active'); }); }); // 设置按钮事件 settingsBtn.addEventListener('click', () => { settingsPanel.classList.add('active'); overlay.classList.add('active'); }); closeSettings.addEventListener('click', () => { settingsPanel.classList.remove('active'); overlay.classList.remove('active'); }); overlay.addEventListener('click', () => { settingsPanel.classList.remove('active'); overlay.classList.remove('active'); }); // 设置颜色选项 document.querySelectorAll('.color-option').forEach(option => { option.addEventListener('click', () => { const bgColor = option.dataset.color; const textColor = option.dataset.text || '#333'; document.documentElement.style.setProperty('--bg-color', bgColor); document.documentElement.style.setProperty('--text-color', textColor); document.querySelectorAll('.color-option').forEach(opt => { opt.classList.remove('active'); }); option.classList.add('active'); }); }); // 设置字体大小选项 document.querySelectorAll('.font-size-options .font-option').forEach(option => { option.addEventListener('click', () => { const fontSize = option.dataset.size; document.documentElement.style.setProperty('--font-size', fontSize); document.querySelectorAll('.font-size-options .font-option').forEach(opt => { opt.classList.remove('active'); }); option.classList.add('active'); }); }); // 设置行间距选项 document.querySelectorAll('.spacing-options .font-option[data-lineheight]').forEach(option => { option.addEventListener('click', () => { const lineHeight = option.dataset.lineheight; document.documentElement.style.setProperty('--line-height', lineHeight); document.querySelectorAll('.spacing-options .font-option[data-lineheight]').forEach(opt => { opt.classList.remove('active'); }); option.classList.add('active'); }); }); // 设置字间距选项 document.querySelectorAll('.spacing-options .font-option[data-letterspacing]').forEach(option => { option.addEventListener('click', () => { const letterSpacing = option.dataset.letterspacing; document.documentElement.style.setProperty('--letter-spacing', letterSpacing); document.querySelectorAll('.spacing-options .font-option[data-letterspacing]').forEach(opt => { opt.classList.remove('active'); }); option.classList.add('active'); }); }); // 设置字体选项 document.querySelectorAll('.font-family-options .font-option').forEach(option => { option.addEventListener('click', () => { const fontFamily = option.dataset.font; document.documentElement.style.setProperty('--font-family', fontFamily); document.querySelectorAll('.font-family-options .font-option').forEach(opt => { opt.classList.remove('active'); }); option.classList.add('active'); }); }); // 导航函数 function navigateToPage(page) { if (!page) return; // 移除所有页面的活动状态 pages.forEach(p => p.classList.remove('active')); // 设置当前页面的活动状态 const currentPageElement = document.querySelector(\`.page[data-page="\${page}"]\`); if (currentPageElement) { currentPageElement.classList.add('active'); currentPage = page; updatePageButtons(); // 更新目录中的活动项 document.querySelectorAll('.toc-item').forEach(item => { if (item.dataset.page === page) { item.classList.add('active'); } else { item.classList.remove('active'); } }); // 滚动到页面顶部 currentPageElement.scrollTop = 0; } } // 获取上一页 function getPreviousPage() { if (currentPage === 'cover') return null; return currentPage === 1 ? 'cover' : (parseInt(currentPage) - 1).toString(); } // 获取下一页 function getNextPage() { if (currentPage === 'cover') return '1'; return parseInt(currentPage) < ${this.content.length} ? (parseInt(currentPage) + 1).toString() : null; } // 更新导航按钮状态 function updatePageButtons() { prevBtn.disabled = !getPreviousPage(); if (prevBtn.disabled) { prevBtn.style.opacity = '0.5'; } else { prevBtn.style.opacity = '1'; } nextBtn.disabled = !getNextPage(); if (nextBtn.disabled) { nextBtn.style.opacity = '0.5'; } else { nextBtn.style.opacity = '1'; } } }); </script> </body> </html>`; return content; } }, 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 = '<div class="po18-card">暂无下载记录</div>'; return; } let html = ''; this.downloadRecords.forEach((record) => { // 添加封面显示 const coverHtml = record.cover ? `<img src="${record.cover}" alt="${record.title}封面" style="width: 60px; height: 80px; object-fit: cover; border-radius: 4px; margin-right: 10px;">` : ''; // 添加标签显示 let tagsHtml = ''; if (record.tags) { const tagsList = record.tags.split('·'); tagsHtml = '<div style="margin-top: 5px;">'; tagsList.forEach(tag => { if (tag.trim()) { tagsHtml += `<span class="po18-book-tag">${tag.trim()}</span> `; } }); tagsHtml += '</div>'; } html += ` <div class="po18-record-item"> <div style="display: flex;"> ${coverHtml} <div style="flex: 1;"> <h4>${record.title || "未知标题"}</h4> <div class="po18-record-info"> <span>作者: ${record.author || "未知作者"}</span> <span>格式: ${record.format ? record.format.toUpperCase() : "未知格式"}</span> </div> <div class="po18-record-info"> <span>大小: ${record.size || "未知大小"}</span> <span>章节数: ${record.chapterCount || "未知"}</span> </div> <div class="po18-record-info"> <span>时间: ${record.time || "未知时间"}</span> <span>耗时: ${record.duration || "0"}秒</span> </div> ${tagsHtml} </div> </div> </div>`; }); container.innerHTML = html; } }; // 初始化下载器 Po18Downloader.init(); })();