// ==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 = `
下载进度
0/0 章节 (0%)
已用时间: 0秒
PO18小说下载器增强版 v1.6.0
这是一款用于下载PO18网站小说的工具,支持TXT/HTML/EPUB格式下载,WebDAV上传等功能。
作者github:wenmoux:
新增功能:
- 全新的粉色主题界面
- 显示小说封面、作者和标签
- 增强HTML输出,支持电子书式的左右翻页
- 阅读界面支持字体大小、颜色主题调整
- 新增行间距、字间距调整功能
- 优化正文排版和阅读舒适度
- 新增书架功能,便于管理已购买小说
- epub下载
- webdav上传
使用方法:
- 在小说页面点击悬浮按钮
- 选择下载格式和线程数
- 点击"开始下载"按钮
注意:需要先登录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.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.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.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 ?
`
` : '';
// 添加标签显示
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();
})();