// ==UserScript== // @name 轻小说文库下载 (优化版) // @namespace wenku8_dl_ug // @version 2.3.2 // @author HaoaW (Original) raventu (Refactor) // @description wenku8_dl_ug is a userscript for downloading Wenku8 novels. wenku8 下载器, 支持批量下载、EPUB格式转换、简繁体转换等功能。 // @icon https://www.wenku8.net/favicon.ico // @source https://github.com/Raven-tu/wenku8_dl_ug // @match *://www.wenku8.net/* // @match *://www.wenku8.cc/* // @require https://cdn.jsdelivr.net/npm/opencc-js@1.0.5/dist/umd/full.js // @require https://cdn.jsdelivr.net/npm/jszip@2.6.1/dist/jszip.min.js // @connect wenku8.com // @connect wenku8.cc // @connect app.wenku8.com // @connect dl.wenku8.com // @connect img.wenku8.com // @grant GM_info // @grant GM_xmlhttpRequest // @grant unsafeWindow // @downloadURL https://update.greasyfork.icu/scripts/536585/%E8%BD%BB%E5%B0%8F%E8%AF%B4%E6%96%87%E5%BA%93%E4%B8%8B%E8%BD%BD%20%28%E4%BC%98%E5%8C%96%E7%89%88%29.user.js // @updateURL https://update.greasyfork.icu/scripts/536585/%E8%BD%BB%E5%B0%8F%E8%AF%B4%E6%96%87%E5%BA%93%E4%B8%8B%E8%BD%BD%20%28%E4%BC%98%E5%8C%96%E7%89%88%29.meta.js // ==/UserScript== (function (JSZip$1) { 'use strict'; var _GM_xmlhttpRequest = /* @__PURE__ */ (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)(); var _unsafeWindow = /* @__PURE__ */ (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)(); const OpenCCConverter = { COOKIE_KEY: "OpenCCwenku8", TARGET_ENCODING_COOKIE_KEY: "targetEncodingCookie", // 假设页面脚本定义了这个key COOKIE_DAYS: 7, buttonElement: null, isConversionEnabled: false, originalSimplized: null, originalTraditionalized: null, /** * 初始化 OpenCC 转换功能 * @param {object} pageGlobals - 包含页面全局变量的对象 (如 unsafeWindow) */ init(pageGlobals) { this.originalSimplized = pageGlobals.Simplized; this.originalTraditionalized = pageGlobals.Traditionalized; if (typeof pageGlobals.OpenCC === "undefined") { console.warn("OpenCC库未加载,简繁转换功能可能受限。"); return; } this.isConversionEnabled = this.getCookie(this.COOKIE_KEY, pageGlobals) === "1"; this.buttonElement = document.createElement("a"); this.buttonElement.href = "javascript:void(0);"; this.buttonElement.addEventListener("click", () => this.toggleConversion(pageGlobals)); this.updateButtonText(); if (this.isConversionEnabled) { if (typeof pageGlobals.Traditionalized !== "undefined") { pageGlobals.Traditionalized = pageGlobals.OpenCC.Converter({ from: "cn", to: "tw" }); } if (typeof pageGlobals.Simplized !== "undefined") { pageGlobals.Simplized = pageGlobals.OpenCC.Converter({ from: "tw", to: "cn" }); } if (this.getCookie(this.TARGET_ENCODING_COOKIE_KEY, pageGlobals) === "2" && typeof pageGlobals.translateBody === "function") { pageGlobals.targetEncoding = "2"; pageGlobals.translateBody(); } } const translateButton = document.querySelector(`#${pageGlobals.translateButtonId}`); if (translateButton && translateButton.parentElement) { translateButton.parentElement.appendChild(document.createTextNode(" ")); translateButton.parentElement.appendChild(this.buttonElement); } else { console.warn("未能找到页面简繁转换按钮的挂载点。OpenCC切换按钮可能无法显示。"); } }, toggleConversion(pageGlobals) { if (this.isConversionEnabled) { this.setCookie(this.COOKIE_KEY, "", this.COOKIE_DAYS, pageGlobals); } else { this.setCookie(this.TARGET_ENCODING_COOKIE_KEY, "2", this.COOKIE_DAYS, pageGlobals); this.setCookie(this.COOKIE_KEY, "1", this.COOKIE_DAYS, pageGlobals); } location.reload(); }, updateButtonText() { this.buttonElement.innerHTML = this.isConversionEnabled ? "关闭(OpenCC)" : "开启(OpenCC)"; }, // 保持与旧代码一致的 setCookie 和 getCookie (可能由页面提供) setCookie(name2, value, days, pageGlobals) { if (typeof pageGlobals.setCookie === "function") { pageGlobals.setCookie(name2, value, days); } else { console.warn("pageGlobals.setCookie 未定义,OpenCC cookie 可能无法设置"); let expires = ""; if (days) { const date = /* @__PURE__ */ new Date(); date.setTime(date.getTime() + days * 24 * 60 * 60 * 1e3); expires = `; expires=${date.toUTCString()}`; } document.cookie = `${name2}=${value || ""}${expires}; path=/`; } }, getCookie(name2, pageGlobals) { if (typeof pageGlobals.getCookie === "function") { return pageGlobals.getCookie(name2); } console.warn("pageGlobals.getCookie 未定义,OpenCC cookie 可能无法读取"); const nameEQ = `${name2}=`; const ca = document.cookie.split(";"); for (let i = 0; i < ca.length; i++) { let c = ca[i]; while (c.charAt(0) === " ") c = c.substring(1, c.length); if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); } return null; } }; const UILogger = { _progressElements: null, // DOM elements for progress display _showButton: null, // Button to show the progress bar after closing init() { if (this._progressElements) return; this._progressElements = { text: document.createElement("span"), image: document.createElement("span"), // Not directly used in main display but useful to hold counts error: document.createElement("div"), // Use a div to hold log entries main: document.createElement("div"), // Main container div controls: document.createElement("div") // Container for control buttons }; this._progressElements.main.id = "epubDownloaderProgress"; this._progressElements.main.style.cssText = "position: fixed; bottom: 0; left: 0; width: 100%; background-color: #f0f0f0; border-top: 1px solid #ccc; padding: 5px; z-index: 9999; font-size: 12px; color: #333; font-family: sans-serif;"; this._progressElements.error.style.cssText = "max-height: 100px; overflow-y: auto; margin-top: 5px; border-top: 1px dashed #ccc; padding-top: 5px;"; this._progressElements.controls.style.cssText = "position: absolute;right: 15px;top: 5px;display: flex;justify-content: flex-end;gap: 5px;margin-bottom: 5px;"; const closeButton = document.createElement("button"); closeButton.textContent = "-"; closeButton.id = "closeProgress"; this._progressElements.controls.appendChild(closeButton); this._showButton = document.createElement("button"); this._showButton.textContent = "+"; this._showButton.style.cssText = "position: fixed; bottom: 10px; right: 10px; z-index: 9999; display: none;"; this._showButton.id = "showProgressButton"; this._progressElements.main.appendChild(this._progressElements.controls); const titleElement = document.getElementById("title"); if (titleElement && titleElement.parentElement) { titleElement.parentElement.insertBefore(this._progressElements.main, titleElement.nextSibling); } else { document.body.appendChild(this._progressElements.main); console.warn("[UILogger] Could not find #title element for progress bar insertion."); } document.body.appendChild(this._showButton); closeButton.addEventListener("click", () => this.closeProgress()); this._showButton.addEventListener("click", () => this.showProgress()); this.clearLog(); this.updateProgress({ Text: [], Images: [], totalTasksAdded: 0, tasksCompletedOrSkipped: 0 }, "ePub下载器就绪..."); }, _ensureInitialized() { if (!this._progressElements || !document.getElementById("epubDownloaderProgress")) { this.init(); } }, closeProgress() { this._progressElements.main.style.display = "none"; this._showButton.style.display = "block"; }, showProgress() { this._progressElements.main.style.display = "block"; this._showButton.style.display = "none"; }, // updateProgress needs access to the bookInfo instance for counts updateProgress(bookInfoInstance, message) { this._ensureInitialized(); if (message) { const time = (/* @__PURE__ */ new Date()).toLocaleTimeString(); const logEntry = document.createElement("div"); logEntry.className = "epub-log-entry"; logEntry.innerHTML = `[${time}] ${message}`; this._progressElements.error.insertBefore(logEntry, this._progressElements.error.firstChild); while (this._progressElements.error.children.length > 300) { this._progressElements.error.removeChild(this._progressElements.error.lastChild); } } const textDownloaded = bookInfoInstance.Text.filter((t) => t.content).length; const totalTexts = bookInfoInstance.Text.length; const imagesDownloaded = bookInfoInstance.Images.filter((img) => img.content).length; const totalImages = bookInfoInstance.Images.length; const totalTasks = bookInfoInstance.totalTasksAdded; const completedTasks = bookInfoInstance.tasksCompletedOrSkipped; const progressHtml = ` ePub生成进度: 文本 ${textDownloaded}/${totalTexts}; 图片 ${imagesDownloaded}/${totalImages}; 任务 ${completedTasks}/${totalTasks};
最新日志: `; this._progressElements.main.innerHTML = progressHtml; this._progressElements.main.appendChild(this._progressElements.controls); if (this._progressElements.error.firstChild) { const latestLogClone = this._progressElements.error.firstChild.cloneNode(true); latestLogClone.style.display = "inline"; latestLogClone.style.fontWeight = "bold"; this._progressElements.main.appendChild(latestLogClone); } else { this._progressElements.main.appendChild(document.createTextNode("无")); } if (!this._progressElements.main.contains(this._progressElements.error)) { this._progressElements.main.appendChild(this._progressElements.error); } }, logError(message) { this._ensureInitialized(); console.error(`[UILogger] ${message}`); this.updateProgress(this.getMinimalBookInfo(), `错误: ${message}`); }, logWarn(message) { this._ensureInitialized(); console.warn(`[UILogger] ${message}`); this.updateProgress(this.getMinimalBookInfo(), `警告: ${message}`); }, logInfo(message) { this._ensureInitialized(); console.log(`[UILogger] ${message}`); this.updateProgress(this.getMinimalBookInfo(), `${message}`); }, clearLog() { this._ensureInitialized(); this._progressElements.error.innerHTML = ""; this._progressElements.main.innerHTML = "ePub生成进度: 文本 0/0;图片 0/0;任务 0/0;
最新日志: 无"; this._progressElements.main.appendChild(this._progressElements.controls); if (!this._progressElements.main.contains(this._progressElements.error)) { this._progressElements.main.appendChild(this._progressElements.error); } }, getMinimalBookInfo() { return this._bookInfoInstance || { Text: [], Images: [], totalTasksAdded: 0, tasksCompletedOrSkipped: 0 }; } }; const name = "wenku8_dl_ug"; const version = "2.3.2"; const author = "HaoaW (Original) raventu (Refactor)"; const repository = { "url": "https://github.com/Raven-tu/wenku8_dl_ug" }; const Package = { name, version, author, repository }; const CURRENT_URL = new URL(_unsafeWindow.location.href); const EPUB_EDITOR_CONFIG_UID = "24A08AE1-E132-458C-9E1D-6C998F16A666"; const IMG_LOCATION_FILENAME = "ImgLocation"; const XML_ILLEGAL_CHARACTERS_REGEX = /[\x00-\x08\v\f\x0E-\x1F]/g; const APP_API_DOMAIN = "app.wenku8.com"; const APP_API_PATH = "/android.php"; const DOWNLOAD_DOMAIN = "dl.wenku8.com"; const IMAGE_DOMAIN = "img.wenku8.com"; const MAX_XHR_RETRIES = 3; const XHR_TIMEOUT_MS = 2e4; const XHR_RETRY_DELAY_MS = 500; const VOLUME_ID_PREFIX = "Volume"; const IMAGE_FILE_PREFIX = "Img"; const TEXT_SPAN_PREFIX = "Txt"; const PROJECT_NAME = Package.name; const PROJECT_AUTHOR = Package.author; const PROJECT_VERSION = Package.version; const PROJECT_REPO = Package.repository.url; const XHRDownloadManager = { _queue: [], _activeDownloads: 0, _maxConcurrentDownloads: 4, // 并发下载数控制 _bookInfoInstance: null, // 关联的EpubBuilder实例 hasCriticalFailure: false, // 标记是否有关键下载失败 init(bookInfoInstance) { this._bookInfoInstance = bookInfoInstance; this._queue = []; this._activeDownloads = 0; this.hasCriticalFailure = false; }, /** * 添加一个下载任务到队列 * @param {object} xhrTask - 任务对象 {url, loadFun, data?, type?, isCritical?} * - url: 请求URL (可选,对于非URL任务如appChapterList) * - loadFun: 实际执行下载的异步函数 (接收 xhrTask 作为参数) * - data: 任务相关数据 * - type: 任务类型 (用于日志和判断关键性) * - isCritical: 是否为关键任务 (默认为true,图片等非关键任务可设为false) */ add(xhrTask) { if (this.hasCriticalFailure) { this._bookInfoInstance.logger.logWarn(`关键下载已失败,新任务 ${xhrTask.type || xhrTask.url} 被跳过。`); this._bookInfoInstance.totalTasksAdded++; this._bookInfoInstance.tasksCompletedOrSkipped++; this._bookInfoInstance.tryBuildEpub(); return; } xhrTask.XHRRetryCount = 0; xhrTask.isCritical = xhrTask.isCritical !== void 0 ? xhrTask.isCritical : true; this._queue.push(xhrTask); this._bookInfoInstance.totalTasksAdded++; this._processQueue(); }, _processQueue() { if (this.hasCriticalFailure) return; while (this._activeDownloads < this._maxConcurrentDownloads && this._queue.length > 0) { const task = this._queue.shift(); this._activeDownloads++; task.loadFun(task).then(() => { }).catch((err2) => { this._bookInfoInstance.logger.logError(`任务 ${task.type || task.url} 执行时发生意外错误: ${err2}`); task.done = true; this.taskFinished(task, task.isCritical); }); } }, /** * 任务完成(成功或最终失败)时调用 * @param {object} task - 完成的任务对象 * @param {boolean} [isFinalFailure] - 任务是否最终失败 */ taskFinished(task, isFinalFailure = false) { if (task._finished) return; task._finished = true; this._activeDownloads--; this._bookInfoInstance.tasksCompletedOrSkipped++; if (isFinalFailure && task.isCritical && !this.hasCriticalFailure) { this.hasCriticalFailure = true; this._bookInfoInstance.XHRFail = true; this._bookInfoInstance.logger.logError("一个关键下载任务最终失败,后续部分任务可能被取消。"); this._queue = []; } this._processQueue(); this._bookInfoInstance.tryBuildEpub(); }, /** * 任务需要重试时调用 * @param {object} xhrTask - 需要重试的任务对象 * @param {string} message - 重试原因消息 */ retryTask(xhrTask, message) { if (this.hasCriticalFailure) { this._bookInfoInstance.logger.logWarn(`重试 ${xhrTask.type || xhrTask.url} 被跳过,因为关键下载已失败。`); xhrTask.done = true; this.taskFinished(xhrTask, true); return; } xhrTask.XHRRetryCount = (xhrTask.XHRRetryCount || 0) + 1; if (xhrTask.XHRRetryCount <= MAX_XHR_RETRIES) { this._bookInfoInstance.refreshProgress(this._bookInfoInstance, `${message} (尝试次数 ${xhrTask.XHRRetryCount}/${MAX_XHR_RETRIES})`); this._activeDownloads--; this._queue.unshift(xhrTask); setTimeout(() => this._processQueue(), XHR_RETRY_DELAY_MS * xhrTask.XHRRetryCount); } else { this._bookInfoInstance.refreshProgress(this._bookInfoInstance, `${xhrTask.type || xhrTask.url} 超出最大重试次数, 下载失败!`); xhrTask.done = true; this.taskFinished(xhrTask, xhrTask.isCritical); } }, /** * 检查所有任务是否完成 (包括队列中和正在进行的) * @returns {boolean} */ areAllTasksDone() { return this._bookInfoInstance.tasksCompletedOrSkipped >= this._bookInfoInstance.totalTasksAdded && this._queue.length === 0 && this._activeDownloads === 0; } }; function gmXmlHttpRequestAsync(details) { return new Promise((resolve, reject) => { _GM_xmlhttpRequest({ ...details, onload: resolve, onerror: (err2) => { console.error(`GM_xmlhttpRequest error for ${details.url}:`, err2); reject(err2); }, ontimeout: (err2) => { console.error(`GM_xmlhttpRequest timeout for ${details.url}:`, err2); reject(err2); } }); }); } async function fetchAsText(url, encoding = "gbk") { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status} for ${url}`); } const buffer = await response.arrayBuffer(); const decoder = new TextDecoder(encoding); return decoder.decode(buffer); } function cleanXmlIllegalChars(text) { return text.replace(XML_ILLEGAL_CHARACTERS_REGEX, ""); } const AppApiService = { volumeChapterData: /* @__PURE__ */ new Map(), // 存储卷的章节列表 { vid: [{cid, cName, content}]} chapterListXml: null, // 缓存书籍的章节列表XML Document isChapterListLoading: false, chapterListWaitQueue: [], // 等待章节列表的请求xhr包装对象 disableTraditionalChineseRequest: true, // 默认禁用APP接口请求繁体,由前端OpenCC处理 _getApiLanguageParam(bookInfo) { const targetEncoding = (bookInfo == null ? void 0 : bookInfo.targetEncoding) || _unsafeWindow.targetEncoding; if (this.disableTraditionalChineseRequest || !targetEncoding) { return "0"; } return targetEncoding === "1" ? "1" : "0"; }, _encryptRequestBody(body) { return `appver=1.0&timetoken=${Number(/* @__PURE__ */ new Date())}&request=${btoa(body)}`; }, /** * 从App接口获取书籍章节列表XML * @param {EpubBuilderCoordinator} bookInfo - 协调器实例 * @returns {Promise} */ async _fetchChapterList(bookInfo) { if (this.isChapterListLoading) return; this.isChapterListLoading = true; bookInfo.refreshProgress(bookInfo, `下载App章节目录...`); const langParam = this._getApiLanguageParam(bookInfo); const requestBody = this._encryptRequestBody(`action=book&do=list&aid=${bookInfo.aid}&t=${langParam}`); const url = `http://${APP_API_DOMAIN}${APP_API_PATH}`; try { const response = await gmXmlHttpRequestAsync({ method: "POST", url, headers: { "content-type": "application/x-www-form-urlencoded;charset=utf-8" }, data: requestBody, timeout: XHR_TIMEOUT_MS }); if (response.status === 200) { const parser = new DOMParser(); this.chapterListXml = parser.parseFromString(cleanXmlIllegalChars(response.responseText), "application/xml"); bookInfo.refreshProgress(bookInfo, `App章节目录下载完成。`); this.chapterListWaitQueue.forEach((queuedXhr) => this.loadVolumeChapters(queuedXhr)); this.chapterListWaitQueue = []; } else { throw new Error(`Status ${response.status}`); } } catch (error) { bookInfo.logger.logError(`App章节目录下载失败: ${error.message}`); bookInfo.XHRManager.taskFinished({ type: "appChapterList", isCritical: true }, true); } finally { this.isChapterListLoading = false; } }, /** * 加载指定分卷的章节内容 (从App接口) * @param {object} xhrVolumeRequest - 卷的XHR任务对象 (由VolumeLoader或Coordinator创建) * - bookInfo: 协调器实例 * - data: { vid, vcssText, Text } * - dealVolume: 处理函数 (通常是 VolumeLoader.dealVolumeText) */ async loadVolumeChapters(xhrVolumeRequest) { const { bookInfo, data: volumeData } = xhrVolumeRequest; if (!this.chapterListXml) { this.chapterListWaitQueue.push(xhrVolumeRequest); if (!this.isChapterListLoading) { this._fetchChapterList(bookInfo); } return; } const volumeElement = Array.from(this.chapterListXml.getElementsByTagName("volume")).find((vol) => vol.getAttribute("vid") === volumeData.vid); if (!volumeElement) { bookInfo.refreshProgress(bookInfo, `App章节目录未找到分卷 ${volumeData.vid},无法生成ePub。`); bookInfo.XHRManager.taskFinished(xhrVolumeRequest, true); return; } const chapters = Array.from(volumeElement.children).map((ch) => ({ cid: ch.getAttribute("cid"), cName: ch.textContent, content: null // 稍后填充 })); this.volumeChapterData.set(volumeData.vid, chapters); chapters.forEach((chapter) => { const chapterXhr = { type: "appChapter", // 自定义类型 url: `http://${APP_API_DOMAIN}${APP_API_PATH}`, loadFun: (xhr) => this._fetchChapterContent(xhr), // 指向内部方法 dealVolume: xhrVolumeRequest.dealVolume, // 传递处理函数 data: { ...volumeData, cid: chapter.cid, cName: chapter.cName, isAppApi: true }, bookInfo, isCritical: true // 章节内容是关键任务 }; bookInfo.XHRManager.add(chapterXhr); }); bookInfo.XHRManager.taskFinished(xhrVolumeRequest, false); }, /** * 从App接口获取单个章节内容 * @param {object} xhrChapterRequest - 章节的XHR任务对象 * - bookInfo: 协调器实例 * - data: { vid, cid, cName, isAppApi, Text } * - dealVolume: 处理函数 (VolumeLoader.dealVolumeText) */ async _fetchChapterContent(xhrChapterRequest) { const { bookInfo, data } = xhrChapterRequest; const langParam = this._getApiLanguageParam(bookInfo); const requestBody = this._encryptRequestBody(`action=book&do=text&aid=${bookInfo.aid}&cid=${data.cid}&t=${langParam}`); const failureMessage = `${data.cName} 下载失败`; try { const response = await gmXmlHttpRequestAsync({ method: "POST", url: xhrChapterRequest.url, headers: { "content-type": "application/x-www-form-urlencoded;charset=utf-8" }, data: requestBody, timeout: XHR_TIMEOUT_MS }); if (response.status === 200) { const chapterVolumeData = this.volumeChapterData.get(data.vid); const chapterEntry = chapterVolumeData.find((c) => c.cid === data.cid); if (chapterEntry) { chapterEntry.content = response.responseText; } bookInfo.refreshProgress(bookInfo, `${data.cName} 下载完成。`); if (chapterVolumeData.every((c) => c.content !== null)) { let combinedVolumeText = ""; for (const chap of chapterVolumeData) { if (!chap.content) continue; let content = chap.content; content = content.replace(chap.cName, `
${chap.cName}
`); content = content.replace(/\r\n/g, "
\r\n"); if (content.includes("http")) { content = content.replace(/(http[\w:/.?@#&=%]+)/g, (match, p1) => `
`); } content += `
`; combinedVolumeText += content; } const pseudoVolumeXhr = { bookInfo, VolumeIndex: bookInfo.Text.findIndex((t) => t.vid === data.vid), // 找到对应的卷索引 data: { ...data, Text: bookInfo.Text.find((t) => t.vid === data.vid) } // 传递卷的Text对象 }; xhrChapterRequest.dealVolume(pseudoVolumeXhr, combinedVolumeText); } bookInfo.XHRManager.taskFinished(xhrChapterRequest, false); } else { throw new Error(`Status ${response.status}`); } } catch (error) { bookInfo.logger.logError(`${failureMessage} 错误: ${error.message}`); bookInfo.XHRManager.retryTask(xhrChapterRequest, failureMessage); } }, /** * 对外暴露的接口,用于从App接口加载章节内容(通常用于版权受限页面) * @param {object} bookInfo - 包含 aid, logger, refreshProgress 的对象 * @param {string} chapterId - 章节ID * @param {HTMLElement} contentElement - 显示内容的DOM元素 * @param {Function} translateBodyFunc - 页面提供的翻译函数 */ async fetchChapterForReading(bookInfo, chapterId, contentElement, translateBodyFunc) { const langParam = this._getApiLanguageParam({ targetEncoding: _unsafeWindow.targetEncoding }); const requestBody = this._encryptRequestBody(`action=book&do=text&aid=${bookInfo.aid}&cid=${chapterId}&t=${langParam}`); const url = `http://${APP_API_DOMAIN}${APP_API_PATH}`; contentElement.innerHTML = "正在通过App接口加载内容,请稍候..."; try { const response = await gmXmlHttpRequestAsync({ method: "POST", url, headers: { "content-type": "application/x-www-form-urlencoded;charset=utf-8" }, data: requestBody, timeout: XHR_TIMEOUT_MS }); if (response.status === 200) { let rawText = response.responseText; rawText = rawText.replace(/ {2}\S.*/, ""); rawText = rawText.replace(/\r\n/g, "
\r\n"); if (rawText.includes("http")) { rawText = rawText.replace(/(http[\w:/.?@#&=%]+)/g, (m, p1) => `
`); } contentElement.innerHTML = rawText; if (typeof translateBodyFunc === "function") { translateBodyFunc(contentElement); } } else { contentElement.innerHTML = `通过App接口加载内容失败,状态码: ${response.status}`; } } catch (error) { contentElement.innerHTML = `通过App接口加载内容失败: ${error.message}`; console.error("App接口内容加载失败:", error); } } }; const VolumeLoader = { /** * 加载网页版分卷文本内容 * @param {object} xhr - XHR任务对象 * - bookInfo: 协调器实例 * - url: 下载URL * - VolumeIndex: 卷在 bookInfo.nav_toc 和 bookInfo.Text 中的索引 * - data: { vid, vcssText, Text } */ async loadWebVolumeText(xhr) { const { bookInfo, url, VolumeIndex, data } = xhr; const volumeInfo = bookInfo.nav_toc[VolumeIndex]; const failureMessage = `${volumeInfo.volumeName} 下载失败`; try { const response = await gmXmlHttpRequestAsync({ method: "GET", url, timeout: XHR_TIMEOUT_MS }); if (response.status === 200) { bookInfo.refreshProgress(bookInfo, `${volumeInfo.volumeName} 网页版内容下载完成。`); this.dealVolumeText(xhr, response.responseText); bookInfo.XHRManager.taskFinished(xhr, false); } else if (response.status === 404) { bookInfo.refreshProgress(bookInfo, `${volumeInfo.volumeName} 网页版404,尝试使用App接口下载...`); xhr.dealVolume = this.dealVolumeText.bind(this); AppApiService.loadVolumeChapters(xhr); bookInfo.XHRManager.taskFinished(xhr, false); } else { throw new Error(`Status ${response.status}`); } } catch (error) { bookInfo.logger.logError(`${failureMessage} 错误: ${error.message}`); bookInfo.XHRManager.retryTask(xhr, failureMessage); } }, /** * 加载书籍内容简介 * @param {object} xhr - XHR任务对象 * - bookInfo: 协调器实例 * - url: 下载URL */ async loadBookDescription(xhr) { const { bookInfo, url } = xhr; const failureMessage = `内容简介下载失败`; try { const text = await fetchAsText(url, "gbk"); const parser = new DOMParser(); const doc = parser.parseFromString(text, "text/html"); const descSpan = doc.evaluate("//span[@class='hottext' and contains(text(),'内容简介:')]/following-sibling::span", doc, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue; if (descSpan) { bookInfo.description = descSpan.textContent.trim(); } bookInfo.refreshProgress(bookInfo, `内容简介下载完成。`); bookInfo.XHRManager.taskFinished(xhr, false); } catch (error) { bookInfo.logger.logError(`${failureMessage} 错误: ${error.message}`); bookInfo.XHRManager.retryTask(xhr, failureMessage); } }, /** * 加载图片资源 * @param {object} xhr - XHR任务对象 * - bookInfo: 协调器实例 * - url: 图片URL * - images: 图片条目信息 {path, content, id, idName, TextId, coverImgChk?, smallCover?} */ async loadImage(xhr) { const { bookInfo, url, images: imageInfo } = xhr; const failureMessage = `${imageInfo.idName} 下载失败`; try { const response = await gmXmlHttpRequestAsync({ method: "GET", url, responseType: "arraybuffer", timeout: XHR_TIMEOUT_MS }); if (response.status === 200) { imageInfo.content = response.response; if (imageInfo.coverImgChk && !bookInfo.Images.some((i) => i.coverImg)) { try { imageInfo.Blob = new Blob([imageInfo.content], { type: "image/jpeg" }); imageInfo.ObjectURL = URL.createObjectURL(imageInfo.Blob); const img = new Image(); img.onload = () => { imageInfo.coverImg = img.naturalHeight / img.naturalWidth > 1.2; URL.revokeObjectURL(imageInfo.ObjectURL); delete imageInfo.Blob; delete imageInfo.ObjectURL; bookInfo.refreshProgress(bookInfo, `${imageInfo.idName} 下载完成${imageInfo.coverImg ? " (设为封面候选)" : ""}。`); bookInfo.XHRManager.taskFinished(xhr, false); }; img.onerror = () => { URL.revokeObjectURL(imageInfo.ObjectURL); delete imageInfo.Blob; delete imageInfo.ObjectURL; bookInfo.logger.logError(`${imageInfo.idName} 图片对象加载失败。`); bookInfo.XHRManager.taskFinished(xhr, false); }; img.src = imageInfo.ObjectURL; } catch (e) { bookInfo.logger.logError(`${imageInfo.idName} 创建Blob/ObjectURL失败: ${e.message}`); bookInfo.XHRManager.taskFinished(xhr, false); } } else { bookInfo.refreshProgress(bookInfo, `${imageInfo.idName} 下载完成。`); bookInfo.XHRManager.taskFinished(xhr, false); } } else { throw new Error(`Status ${response.status}`); } } catch (error) { bookInfo.logger.logError(`${failureMessage} 错误: ${error.message}`); bookInfo.XHRManager.retryTask(xhr, failureMessage); } }, /** * 处理下载到的分卷文本内容 (网页版或App版合并后的) * @param {object} xhr - 原始的卷XHR任务对象 (或AppService伪造的对象) * - bookInfo: 协调器实例 * - VolumeIndex: 卷在 bookInfo.nav_toc 和 bookInfo.Text 中的索引 * - data: { vid, vcssText, Text } * @param {string} htmlText - 下载到的HTML或合并后的章节文本 */ dealVolumeText(xhr, htmlText) { const { bookInfo, VolumeIndex, data } = xhr; const volumeTextData = data.Text; const navTocEntry = volumeTextData.navToc; let chapterCounter = 0; let imageCounter = 0; let textNodeCounter = 0; const parser = new DOMParser(); const tempDoc = parser.parseFromString( ``, "text/html" ); tempDoc.body.innerHTML = htmlText; if (typeof _unsafeWindow.currentEncoding !== "undefined" && typeof _unsafeWindow.targetEncoding !== "undefined" && _unsafeWindow.currentEncoding !== _unsafeWindow.targetEncoding && typeof _unsafeWindow.translateBody === "function") { _unsafeWindow.translateBody(tempDoc.body); } const elementsToRemove = []; Array.from(tempDoc.body.children).forEach((child) => { if (child.tagName === "UL" && child.id === "contentdp") { elementsToRemove.push(child); } else if (child.tagName === "DIV" && child.className === "chaptertitle") { chapterCounter++; const chapterTitleText = child.textContent.trim(); const chapterDivId = `chapter_${chapterCounter}`; child.innerHTML = `

${cleanXmlIllegalChars(chapterTitleText)}

`; if (navTocEntry) { navTocEntry.chapterArr.push({ chapterName: chapterTitleText, chapterID: chapterDivId, chapterHref: `${navTocEntry.volumeHref}#${chapterDivId}` }); } const titleSpan = tempDoc.createElement("span"); titleSpan.id = `${TEXT_SPAN_PREFIX}_${chapterDivId}`; titleSpan.className = "txtDropEnable"; titleSpan.setAttribute("ondragover", "return false"); child.parentElement.insertBefore(titleSpan, child); titleSpan.appendChild(child); } else if (child.tagName === "DIV" && child.className === "chaptercontent") { Array.from(child.childNodes).forEach((contentNode) => { if (contentNode.nodeType === Node.TEXT_NODE && contentNode.textContent.trim() !== "") { textNodeCounter++; const textSpan = tempDoc.createElement("span"); textSpan.id = `${TEXT_SPAN_PREFIX}_${VolumeIndex}_${textNodeCounter}`; textSpan.className = "txtDropEnable"; textSpan.setAttribute("ondragover", "return false"); child.insertBefore(textSpan, contentNode); textSpan.appendChild(contentNode); } else if (contentNode.tagName === "DIV" && contentNode.className === "divimage" && contentNode.hasAttribute("title")) { const imgSrc = contentNode.getAttribute("title"); if (imgSrc) { const imgUrl = new URL(imgSrc); const imgFileName = imgUrl.pathname.split("/").pop(); const imgPathInEpub = `Images${imgUrl.pathname}`; contentNode.innerHTML = `${cleanXmlIllegalChars(imgFileName)}`; imageCounter++; const imageId = `${IMAGE_FILE_PREFIX}_${VolumeIndex}_${imageCounter}`; const imageEntry = { path: imgPathInEpub, content: null, // 稍后下载 id: imageId, idName: imgFileName, // 文件名作为标识 TextId: volumeTextData.id, // 关联到分卷ID coverImgChk: VolumeIndex === 0 && imageCounter <= 2 // 前两张图作为封面候选 }; if (!bookInfo.Images.some((img) => img.path === imageEntry.path)) { bookInfo.Images.push(imageEntry); bookInfo.XHRManager.add({ type: "image", url: imgSrc, loadFun: (imgXhr) => VolumeLoader.loadImage(imgXhr), // 静态方法调用 images: imageEntry, // 传递图片条目信息 bookInfo, isCritical: false // 图片不是关键任务 }); } } } }); } }); elementsToRemove.forEach((el) => el.parentElement.removeChild(el)); volumeTextData.content = tempDoc.body.innerHTML; if (VolumeIndex === 0 && !bookInfo.thumbnailImageAdded) { const pathParts = CURRENT_URL.pathname.replace("novel", "image").split("/"); pathParts.pop(); const bookNumericId = pathParts.find((p) => /^\d+$/.test(p)); if (bookNumericId) { const thumbnailImageId = `${bookNumericId}s`; const thumbnailSrc = `https://${IMAGE_DOMAIN}${pathParts.join("/")}/${thumbnailImageId}.jpg`; const thumbnailPathInEpub = `Images/${bookNumericId}/${thumbnailImageId}.jpg`; const thumbnailEntry = { path: thumbnailPathInEpub, content: null, id: thumbnailImageId, // 使用图片本身的ID idName: `${thumbnailImageId}.jpg`, TextId: "", // 不关联特定卷,作为通用封面候选 smallCover: true, // 标记为缩略图封面候选 isCritical: false // 缩略图不是关键任务 }; if (!bookInfo.Images.some((img) => img.id === thumbnailEntry.id)) { bookInfo.Images.push(thumbnailEntry); bookInfo.XHRManager.add({ type: "image", url: thumbnailSrc, loadFun: (thumbXhr) => VolumeLoader.loadImage(thumbXhr), images: thumbnailEntry, bookInfo, isCritical: false }); bookInfo.thumbnailImageAdded = true; } } } if (!bookInfo.descriptionXhrInitiated) { bookInfo.descriptionXhrInitiated = true; bookInfo.XHRManager.add({ type: "description", url: `/book/${bookInfo.aid}.htm`, // 相对路径 loadFun: (descXhr) => VolumeLoader.loadBookDescription(descXhr), bookInfo, isCritical: true // 简介是关键任务 }); } bookInfo.tryBuildEpub(); } }; let _global = typeof window === "object" && window.window === window ? window : typeof self === "object" && self.self === self ? self : typeof global === "object" && global.global === global ? global : void 0; function bom(blob, opts) { if (typeof opts === "undefined") { opts = { autoBom: false }; } else if (typeof opts !== "object") { console.warn("Deprecated: Expected third argument to be a object"); opts = { autoBom: !opts }; } if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|[^\s/]*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { return new Blob([String.fromCharCode(65279), blob], { type: blob.type }); } return blob; } function download(url, name2, opts) { let xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.responseType = "blob"; xhr.onload = function() { saveAs(xhr.response, name2, opts); }; xhr.onerror = function() { console.error("could not download file"); }; xhr.send(); } function corsEnabled(url) { let xhr = new XMLHttpRequest(); xhr.open("HEAD", url, false); try { xhr.send(); } catch (e) { } return xhr.status >= 200 && xhr.status <= 299; } function click(node) { try { node.dispatchEvent(new MouseEvent("click")); } catch (e) { let evt = document.createEvent("MouseEvents"); evt.initMouseEvent("click", true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); node.dispatchEvent(evt); } } let isMacOSWebView = _global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent); const saveAs = _global.saveAs || // probably in some web worker (typeof window !== "object" || window !== _global ? function saveAs2() { } : "download" in HTMLAnchorElement.prototype && !isMacOSWebView ? function saveAs3(blob, name2, opts) { let URL2 = _global.URL || _global.webkitURL; let a = document.createElement("a"); name2 = name2 || blob.name || "download"; a.download = name2; a.rel = "noopener"; if (typeof blob === "string") { a.href = blob; if (a.origin !== location.origin) { corsEnabled(a.href) ? download(blob, name2, opts) : click(a, a.target = "_blank"); } else { click(a); } } else { a.href = URL2.createObjectURL(blob); setTimeout(() => { URL2.revokeObjectURL(a.href); }, 4e4); setTimeout(() => { click(a); }, 0); } } : "msSaveOrOpenBlob" in navigator ? function saveAs4(blob, name2, opts) { name2 = name2 || blob.name || "download"; if (typeof blob === "string") { if (corsEnabled(blob)) { download(blob, name2, opts); } else { let a = document.createElement("a"); a.href = blob; a.target = "_blank"; setTimeout(() => { click(a); }); } } else { navigator.msSaveOrOpenBlob(bom(blob, opts), name2); } } : function saveAs5(blob, name2, opts, popup) { popup = popup || open("", "_blank"); if (popup) { popup.document.title = popup.document.body.innerText = "downloading..."; } if (typeof blob === "string") return download(blob, name2, opts); let force = blob.type === "application/octet-stream"; let isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari; let isChromeIOS = /CriOS\/\d+/.test(navigator.userAgent); if ((isChromeIOS || force && isSafari || isMacOSWebView) && typeof FileReader !== "undefined") { let reader = new FileReader(); reader.onloadend = function() { let url = reader.result; url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, "data:attachment/file;"); if (popup) popup.location.href = url; else location = url; popup = null; }; reader.readAsDataURL(blob); } else { let URL2 = _global.URL || _global.webkitURL; let url = URL2.createObjectURL(blob); if (popup) popup.location = url; else location.href = url; popup = null; setTimeout(() => { URL2.revokeObjectURL(url); }, 4e4); } }); const EpubEditor = { novelTableElement: null, config: { UID: EPUB_EDITOR_CONFIG_UID, aid: _unsafeWindow.article_id, // 来自页面全局变量 pathname: CURRENT_URL.pathname, ImgLocation: [] }, imgLocationRegex: [/img/i, /插图/, /插圖/, /\.jpg/i, /\.png/i], styleLinks: [], editorRootElement: null, lastClickedVolumeLI: null, _bookInfoInstance: null, // 保存EpubBuilder实例引用 _currentVolumeImageMap: /* @__PURE__ */ new Map(), // 当前卷可用的图片映射 {idName: imgElement} /** * 初始化ePub编辑器 * @param {EpubBuilderCoordinator} bookInfoInstance - 协调器实例 */ init(bookInfoInstance) { this._bookInfoInstance = bookInfoInstance; document.querySelectorAll(".DownloadAll").forEach((el) => el.style.pointerEvents = "none"); this.novelTableElement = document.body.getElementsByTagName("table")[0]; if (this.novelTableElement) this.novelTableElement.style.display = "none"; const editorCss = document.createElement("link"); editorCss.type = "text/css"; editorCss.rel = "stylesheet"; editorCss.href = "/themes/wenku8/style.css"; document.head.appendChild(editorCss); this.styleLinks.push(editorCss); this.editorRootElement = document.createElement("div"); this.editorRootElement.id = "ePubEidter"; this.editorRootElement.style.display = "none"; editorCss.onload = () => { this.editorRootElement.style.display = ""; }; this.editorRootElement.innerHTML = this._getEditorHtmlTemplate(); if (this.novelTableElement && this.novelTableElement.parentElement) { this.novelTableElement.parentElement.insertBefore(this.editorRootElement, this.novelTableElement); } else { document.body.appendChild(this.editorRootElement); console.warn("未能找到合适的编辑器挂载点,编辑器已追加到body。"); } document.getElementById("EidterBuildBtn").addEventListener("click", (event) => this.handleBuildEpubClick(event)); document.getElementById("EidterImportBtn").addEventListener("click", (event) => this.handleImportConfigClick(event)); document.getElementById("VolumeImg").addEventListener("drop", (event) => this.handleImageDeleteDrop(event)); document.getElementById("VolumeImg").addEventListener("dragover", (event) => event.preventDefault()); this.config.ImgLocation = bookInfoInstance.ImgLocation; document.getElementById("CfgArea").value = JSON.stringify(this.config, null, " "); this._populateVolumeList(); }, /** * 销毁编辑器DOM和事件监听器 */ destroy() { if (this.editorRootElement && this.editorRootElement.parentElement) { this.editorRootElement.parentElement.removeChild(this.editorRootElement); } this.styleLinks.forEach((link) => link.parentElement && link.parentElement.removeChild(link)); if (this.novelTableElement) this.novelTableElement.style.display = ""; document.querySelectorAll(".DownloadAll").forEach((el) => el.style.pointerEvents = "auto"); this.editorRootElement = null; this._bookInfoInstance = null; this._currentVolumeImageMap.clear(); }, /** * 填充分卷列表 */ _populateVolumeList() { const volumeUl = document.getElementById("VolumeUL"); if (!volumeUl) return; volumeUl.innerHTML = ""; let firstLi = null; this._bookInfoInstance.Text.forEach((textEntry) => { const li = document.createElement("li"); const a = document.createElement("a"); a.href = "javascript:void(0);"; a.id = textEntry.id; a.textContent = textEntry.volumeName; li.appendChild(a); li.addEventListener("click", (event) => this.handleVolumeClick(event, textEntry)); volumeUl.appendChild(li); if (!firstLi) firstLi = li; }); if (firstLi) firstLi.click(); }, /** * 处理分卷列表点击事件 * @param {MouseEvent} event * @param {object} textEntry - 当前卷的文本数据 {path, content, id, vid, volumeName, navToc} */ handleVolumeClick(event, textEntry) { if (this.lastClickedVolumeLI) { this.lastClickedVolumeLI.firstElementChild.style.color = ""; } this.lastClickedVolumeLI = event.currentTarget; this.lastClickedVolumeLI.firstElementChild.style.color = "fuchsia"; const volumeTextDiv = document.getElementById("VolumeText"); if (!volumeTextDiv) return; volumeTextDiv.style.display = "none"; volumeTextDiv.innerHTML = textEntry.content; this._populateImageListForVolume(textEntry); this._populateGuessedImageLocations(textEntry); this._populateChapterNavForVolume(textEntry); volumeTextDiv.style.display = ""; volumeTextDiv.scrollTop = 0; const volumeImgDiv = document.getElementById("VolumeImg"); if (volumeImgDiv) volumeImgDiv.scrollTop = 0; }, /** * 填充当前卷可用的图片列表 * @param {object} textEntry - 当前卷的文本数据 */ _populateImageListForVolume(textEntry) { const volumeImgDiv = document.getElementById("VolumeImg"); if (!volumeImgDiv) return; volumeImgDiv.innerHTML = ""; this._currentVolumeImageMap.clear(); this._bookInfoInstance.Images.filter((img) => img.TextId === textEntry.id || img.smallCover).forEach((imageInfo) => { if (!imageInfo.ObjectURL && imageInfo.content) { try { imageInfo.Blob = new Blob([imageInfo.content], { type: "image/jpeg" }); imageInfo.ObjectURL = URL.createObjectURL(imageInfo.Blob); } catch (e) { console.error(`创建图片Blob失败 for ${imageInfo.idName}:`, e); return; } } else if (!imageInfo.ObjectURL && !imageInfo.content) { console.warn(`图片 ${imageInfo.idName} 既无ObjectURL也无content,无法显示。`); return; } const div = document.createElement("div"); div.className = "editor-image-item"; const img = document.createElement("img"); img.setAttribute("imgID", imageInfo.idName); img.src = imageInfo.ObjectURL; img.height = 127; img.draggable = true; img.addEventListener("dragstart", (event) => this.handleImageDragStart(event, imageInfo)); this._currentVolumeImageMap.set(imageInfo.idName, img); div.appendChild(img); div.appendChild(document.createElement("br")); div.appendChild(document.createTextNode(imageInfo.id)); volumeImgDiv.appendChild(div); }); }, /** * 填充推测的插图位置列表并在文本中标记 * @param {object} textEntry - 当前卷的文本数据 */ _populateGuessedImageLocations(textEntry) { const imgUl = document.getElementById("ImgUL"); if (!imgUl) return; imgUl.innerHTML = ""; const currentVolumeLocations = this._bookInfoInstance.ImgLocation.filter((loc) => loc.vid === textEntry.vid); document.querySelectorAll("#VolumeText .txtDropEnable").forEach((dropTargetSpan) => { dropTargetSpan.addEventListener("drop", (event) => this.handleTextDrop(event, textEntry)); dropTargetSpan.addEventListener("dragover", (event) => event.preventDefault()); currentVolumeLocations.filter((loc) => loc.spanID === dropTargetSpan.id).forEach((loc) => { const draggedImgElement = this._currentVolumeImageMap.get(loc.imgID); if (draggedImgElement) { const divImage = document.createElement("div"); divImage.className = "divimageM"; divImage.innerHTML = draggedImgElement.outerHTML; const actualImgInDiv = divImage.firstElementChild; actualImgInDiv.id = `${loc.spanID}_${loc.imgID}`; actualImgInDiv.draggable = true; actualImgInDiv.addEventListener("dragstart", (ev) => this.handlePlacedImageDragStart(ev, loc)); dropTargetSpan.parentNode.insertBefore(divImage, dropTargetSpan); } }); if (!dropTargetSpan.firstElementChild || dropTargetSpan.firstElementChild.className !== "chaptertitle") { for (const regex of this.imgLocationRegex) { if (regex.test(dropTargetSpan.textContent)) { const li = document.createElement("li"); const a = document.createElement("a"); a.href = "javascript:void(0);"; a.setAttribute("SpanID", dropTargetSpan.id); a.textContent = `${dropTargetSpan.textContent.replace(/\s/g, "").substring(0, 12)}...`; li.appendChild(a); li.addEventListener("click", () => this.handleGuessedLocationClick(dropTargetSpan)); imgUl.appendChild(li); dropTargetSpan.style.color = "fuchsia"; break; } } } }); }, /** * 填充章节导航列表 * @param {object} textEntry - 当前卷的文本数据 */ _populateChapterNavForVolume(textEntry) { const chapterUl = document.getElementById("ChapterUL"); if (!chapterUl) return; chapterUl.innerHTML = ""; const tocEntry = this._bookInfoInstance.nav_toc.find((toc) => toc.volumeID === textEntry.id); if (tocEntry && tocEntry.chapterArr) { tocEntry.chapterArr.forEach((chapter) => { const li = document.createElement("li"); const a = document.createElement("a"); a.href = "javascript:void(0);"; a.setAttribute("chapterID", chapter.chapterID); a.textContent = chapter.chapterName; li.appendChild(a); li.addEventListener("click", () => this.handleChapterNavClick(chapter.chapterID)); chapterUl.appendChild(li); }); } }, /** * 处理从图片列表拖动图片开始事件 * @param {DragEvent} event * @param {object} imageInfo - 图片条目信息 */ handleImageDragStart(event, imageInfo) { event.dataTransfer.setData("text/plain", imageInfo.idName); event.dataTransfer.setData("sourceType", "newImage"); }, /** * 处理从文本中拖动已放置图片开始事件 * @param {DragEvent} event * @param {object} imgLocation - 图片位置配置 {vid, spanID, imgID} */ handlePlacedImageDragStart(event, imgLocation) { event.dataTransfer.setData("text/plain", JSON.stringify(imgLocation)); event.dataTransfer.setData("sourceType", "placedImage"); event.dataTransfer.setData("elementId", event.target.id); }, /** * 处理拖动图片到文本区域的放置事件 * @param {DragEvent} event * @param {object} textEntry - 当前卷的文本数据 */ handleTextDrop(event, textEntry) { event.preventDefault(); const sourceType = event.dataTransfer.getData("sourceType"); if (sourceType === "newImage") { const imgIdName = event.dataTransfer.getData("text/plain"); const dropTargetSpan = event.currentTarget; const newLocation = { vid: textEntry.vid, spanID: dropTargetSpan.id, imgID: imgIdName }; if (this._bookInfoInstance.ImgLocation.some((loc) => loc.vid === newLocation.vid && loc.spanID === newLocation.spanID && loc.imgID === newLocation.imgID)) { this._bookInfoInstance.logger.logWarn(`尝试在 ${newLocation.spanID} 放置重复图片 ${newLocation.imgID}。`); return; } this._bookInfoInstance.ImgLocation.push(newLocation); const draggedImgElement = this._currentVolumeImageMap.get(imgIdName); if (draggedImgElement) { const divImage = document.createElement("div"); divImage.className = "divimageM"; divImage.innerHTML = draggedImgElement.outerHTML; const actualImgInDiv = divImage.firstElementChild; actualImgInDiv.id = `${newLocation.spanID}_${newLocation.imgID}`; actualImgInDiv.draggable = true; actualImgInDiv.addEventListener("dragstart", (ev) => this.handlePlacedImageDragStart(ev, newLocation)); dropTargetSpan.parentNode.insertBefore(divImage, dropTargetSpan); } this._updateConfigTextarea(); } }, /** * 处理拖动已放置图片到删除区域的放置事件 * @param {DragEvent} event */ handleImageDeleteDrop(event) { event.preventDefault(); const sourceType = event.dataTransfer.getData("sourceType"); if (sourceType === "placedImage") { const imgLocationJson = event.dataTransfer.getData("text/plain"); const elementIdToRemove = event.dataTransfer.getData("elementId"); try { const locToRemove = JSON.parse(imgLocationJson); this._bookInfoInstance.ImgLocation = this._bookInfoInstance.ImgLocation.filter( (loc) => !(loc.vid === locToRemove.vid && loc.spanID === locToRemove.spanID && loc.imgID === locToRemove.imgID) ); const domElementToRemove = document.getElementById(elementIdToRemove); if (domElementToRemove && domElementToRemove.parentElement.className === "divimageM") { domElementToRemove.parentElement.remove(); } this._updateConfigTextarea(); this._bookInfoInstance.logger.logInfo(`已移除图片 ${locToRemove.imgID} 在 ${locToRemove.spanID} 的位置配置。`); } catch (e) { console.error("解析拖放数据失败:", e); this._bookInfoInstance.logger.logError("删除图片配置失败。"); } } }, /** * 处理推测插图位置列表点击事件 (滚动到文本位置) * @param {HTMLElement} dropTargetSpan - 对应的文本span元素 */ handleGuessedLocationClick(dropTargetSpan) { const volumeTextDiv = document.getElementById("VolumeText"); if (volumeTextDiv && dropTargetSpan) { volumeTextDiv.scroll({ top: dropTargetSpan.offsetTop - 130, behavior: "smooth" }); } }, /** * 处理章节导航列表点击事件 (滚动到章节位置) * @param {string} chapterDomId - 章节对应的DOM ID */ handleChapterNavClick(chapterDomId) { const targetElement = document.getElementById(chapterDomId); if (targetElement) { const volumeTextDiv = document.getElementById("VolumeText"); if (volumeTextDiv) { volumeTextDiv.scroll({ top: targetElement.offsetTop, behavior: "smooth" }); } } }, /** * 处理“生成ePub”按钮点击事件 * @param {MouseEvent} event */ handleBuildEpubClick(event) { var _a, _b; event.currentTarget.disabled = true; this._bookInfoInstance.ePubEidtDone = true; this._bookInfoInstance.tryBuildEpub(); if (((_a = document.getElementById("SendArticle")) == null ? void 0 : _a.checked) && this._bookInfoInstance.ImgLocation.length > 0) { this._sendConfigToServer(); } if ((_b = document.getElementById("ePubEditerClose")) == null ? void 0 : _b.checked) { setTimeout(() => { if (this._bookInfoInstance && this._bookInfoInstance.XHRManager.areAllTasksDone()) { this.destroy(); } else { this._bookInfoInstance.logger.logInfo("ePub生成未完成或失败,编辑器未自动关闭。"); } }, 3e3); } }, /** * 处理“导入配置”按钮点击事件 * @param {MouseEvent} event */ handleImportConfigClick(event) { event.currentTarget.disabled = true; const cfgArea = document.getElementById("CfgArea"); if (!cfgArea) { event.currentTarget.disabled = false; return; } try { const importedCfg = JSON.parse(cfgArea.value); if (importedCfg && importedCfg.UID === this.config.UID && importedCfg.aid === this.config.aid && Array.isArray(importedCfg.ImgLocation) && importedCfg.ImgLocation.length > 0) { let importedCount = 0; importedCfg.ImgLocation.forEach((iCfg) => { if (!this._bookInfoInstance.Text.some((t) => t.vid === iCfg.vid)) { console.warn(`导入配置跳过无效卷ID: ${iCfg.vid}`); return; } if (!iCfg.spanID || !iCfg.imgID) { console.warn(`导入配置跳过无效项: ${JSON.stringify(iCfg)}`); return; } if (!this._bookInfoInstance.ImgLocation.some( (loc) => loc.vid === iCfg.vid && loc.spanID === iCfg.spanID && loc.imgID === iCfg.imgID )) { this._bookInfoInstance.ImgLocation.push({ vid: String(iCfg.vid), // 确保vid是字符串,与内部存储一致 spanID: String(iCfg.spanID), imgID: String(iCfg.imgID) }); importedCount++; } }); this._updateConfigTextarea(); this._bookInfoInstance.logger.logInfo(`成功导入 ${importedCount} 条插图位置配置。`); if (this.lastClickedVolumeLI) this.lastClickedVolumeLI.click(); } else { console.error("导入的配置格式不正确或与当前书籍不匹配。"); } } catch (e) { console.error("导入配置失败:JSON格式错误。"); console.error("导入配置解析错误:", e); } event.currentTarget.disabled = false; }, /** * 更新配置文本框内容 */ _updateConfigTextarea() { this.config.ImgLocation = this._bookInfoInstance.ImgLocation; const cfgArea = document.getElementById("CfgArea"); if (cfgArea) cfgArea.value = JSON.stringify(this.config, null, " "); }, /** * 将配置发送到书评区 */ async _sendConfigToServer() { const cfgToSend = { ...this.config }; const imgLocJson = JSON.stringify(this._bookInfoInstance.ImgLocation); const zip = new JSZip(); zip.file(IMG_LOCATION_FILENAME, imgLocJson, { compression: "DEFLATE", compressionOptions: { level: 9 } }); try { const base64Data = await zip.generate({ type: "base64", mimeType: "application/zip" }); cfgToSend.ImgLocationBase64 = base64Data; delete cfgToSend.ImgLocation; const uniqueVolumeNames = [...new Set( this._bookInfoInstance.ImgLocation.map((loc) => this._bookInfoInstance.nav_toc.find((toc) => toc.vid === loc.vid)).filter(Boolean).map((toc) => toc.volumeName) )]; const postContent = `包含分卷列表:${uniqueVolumeNames.join(", ")} [code]${JSON.stringify(cfgToSend)}[/code]`; const postData = /* @__PURE__ */ new Map([ ["ptitle", "ePub插图位置 (优化版脚本)"], ["pcontent", postContent] ]); const postUrl = `/modules/article/reviews.php?aid=${this._bookInfoInstance.aid}`; const iframe = document.createElement("iframe"); iframe.style.display = "none"; document.body.appendChild(iframe); const iframeDoc = iframe.contentWindow.document; const form = iframeDoc.createElement("form"); form.acceptCharset = "gbk"; form.method = "POST"; form.action = postUrl; postData.forEach((value, key) => { const input = iframeDoc.createElement("input"); input.type = "hidden"; input.name = key; input.value = value; form.appendChild(input); }); iframeDoc.body.appendChild(form); form.submit(); setTimeout(() => iframe.remove(), 5e3); this._bookInfoInstance.logger.logInfo("插图配置已尝试发送到书评区。"); } catch (error) { this._bookInfoInstance.logger.logError(`压缩配置失败,无法发送到书评区: ${err.message}`); console.error("Zip config error:", err); } }, /** * 获取编辑器HTML模板字符串 * @returns {string} HTML模板 */ _getEditorHtmlTemplate() { return `
操作设置
  • 配置内容:
分卷
    推测插图位置
      章节





        (可拖拽图片到下方文本)

        (将已放置图片拖到此处可删除)
        分卷内容 (可拖入图片)
        `; } }; const EpubFileBuilder = { MIMETYPE: "application/epub+zip", CONTAINER_XML: ``, DEFAULT_CSS: { content: `nav#landmarks, nav#page-list { display:none; } ol { list-style-type: none; } .volumetitle, .chaptertitle { text-align: center; } .divimage { text-align: center; margin-top: 0.5em; margin-bottom: 0.5em; } .divimage img { max-width: 100%; height: auto; vertical-align: middle; }`, id: "default_css_id", path: "Styles/default.css" }, NAV_XHTML_TEMPLATE: { content: `目录`, path: `Text/nav.xhtml`, id: `nav_xhtml_id` }, /** * 构建并生成ePub文件 * @param {EpubBuilderCoordinator} bookInfo - 协调器实例 */ async build(bookInfo) { var _a; if (bookInfo.XHRManager.hasCriticalFailure) { bookInfo.refreshProgress(bookInfo, `关键文件下载失败,无法生成ePub。`); const buildBtn = document.getElementById("EidterBuildBtn"); if (buildBtn) buildBtn.disabled = false; return; } if (!bookInfo.XHRManager.areAllTasksDone()) { bookInfo.refreshProgress(bookInfo, `等待下载任务完成... (${bookInfo.tasksCompletedOrSkipped}/${bookInfo.totalTasksAdded})`); return; } if (bookInfo.ePubEidt && !bookInfo.ePubEidtDone) { bookInfo.refreshProgress(bookInfo, `等待用户编辑插图位置...`); if (!EpubEditor.editorRootElement) { EpubEditor.init(bookInfo); } return; } bookInfo.refreshProgress(bookInfo, `开始生成ePub文件...`); _unsafeWindow._isUnderConstruction = true; const zip = new JSZip$1(); zip.file("mimetype", this.MIMETYPE, { compression: "STORE" }); zip.file("META-INF/container.xml", this.CONTAINER_XML); const oebpsFolder = zip.folder("OEBPS"); const contentOpfDoc = this._createContentOpfDocument(bookInfo); const manifest = contentOpfDoc.querySelector("manifest"); const spine = contentOpfDoc.querySelector("spine"); this._addManifestItem(manifest, this.DEFAULT_CSS.id, this.DEFAULT_CSS.path, "text/css"); oebpsFolder.file(this.DEFAULT_CSS.path, this.DEFAULT_CSS.content); const navXhtmlContent = this._generateNavXhtml(bookInfo.nav_toc); const navItem = this._addManifestItem(manifest, this.NAV_XHTML_TEMPLATE.id, this.NAV_XHTML_TEMPLATE.path, "application/xhtml+xml"); navItem.setAttribute("properties", "nav"); this._addSpineItem(spine, this.NAV_XHTML_TEMPLATE.id, "no"); oebpsFolder.file(this.NAV_XHTML_TEMPLATE.path, navXhtmlContent); bookInfo.Text.forEach((textEntry) => { this._addManifestItem(manifest, textEntry.id, textEntry.path, "application/xhtml+xml"); this._addSpineItem(spine, textEntry.id); const finalHtml = this._processAndCleanVolumeHtml(textEntry, bookInfo); oebpsFolder.file(textEntry.path, finalHtml); }); let coverImageId = null; const userCover = bookInfo.ImgLocation.find((loc) => loc.isCover); if (userCover) { const imgEntry = bookInfo.Images.find((img) => img.idName === userCover.imgID); if (imgEntry) coverImageId = imgEntry.id; } if (!coverImageId) { const coverImage = bookInfo.Images.find((img) => img.coverImg) || bookInfo.Images.find((img) => img.coverImgChk) || bookInfo.Images.find((img) => img.smallCover); if (coverImage) coverImageId = coverImage.id; } bookInfo.Images.forEach((imgEntry) => { if (imgEntry.content) { const item = this._addManifestItem(manifest, imgEntry.id, imgEntry.path, "image/jpeg"); if (imgEntry.id === coverImageId) { item.setAttribute("properties", "cover-image"); } oebpsFolder.file(imgEntry.path, imgEntry.content, { binary: true }); } else { bookInfo.logger.logWarn(`图片 ${imgEntry.idName} 内容为空,未打包进ePub。`); } }); if (bookInfo.ImgLocation && bookInfo.ImgLocation.length > 0) { const editorCfg = { UID: EPUB_EDITOR_CONFIG_UID, aid: bookInfo.aid, pathname: CURRENT_URL.pathname, ImgLocation: bookInfo.ImgLocation }; const cfgJson = JSON.stringify(editorCfg, null, " "); oebpsFolder.file("Other/ePubEditorCfg.json", cfgJson); this._addManifestItem(manifest, "editor_cfg_json", "Other/ePubEditorCfg.json", "application/json"); bookInfo.logger.logInfo("编辑器配置已保存到ePub。"); } this._populateMetadata(contentOpfDoc, bookInfo, coverImageId); const serializer = new XMLSerializer(); let contentOpfString = serializer.serializeToString(contentOpfDoc); contentOpfString = contentOpfString.replace(/ xmlns=""/g, ""); const content_opf_final = `${contentOpfString}`; oebpsFolder.file("content.opf", content_opf_final); let epubFileName = `${bookInfo.title}.${bookInfo.nav_toc[0].volumeName}`; if (bookInfo.nav_toc.length > 1) { epubFileName += `-${bookInfo.nav_toc[bookInfo.nav_toc.length - 1].volumeName}`; } epubFileName = epubFileName.replace(/[\\/:*?"<>|]/g, "_"); try { const blob = await zip.generate({ type: "blob", mimeType: "application/epub+zip" }); saveAs(blob, `${epubFileName}.epub`); bookInfo.refreshProgress(bookInfo, `ePub生成完成, 文件名:${epubFileName}.epub`); } catch (err2) { bookInfo.logger.logError(`ePub压缩或保存失败: ${err2.message}`); bookInfo.refreshProgress(bookInfo, `ePub生成失败: ${err2.message}`); } finally { const buildBtn = document.getElementById("EidterBuildBtn"); if (buildBtn) buildBtn.disabled = false; } if (bookInfo.ePubEidtDone && EpubEditor.editorRootElement && ((_a = document.getElementById("ePubEditerClose")) == null ? void 0 : _a.checked)) { setTimeout(() => EpubEditor.destroy(), 1e3); } return true; }, /** * 创建 content.opf 的 DOM 文档 * @param {EpubBuilderCoordinator} bookInfo - 协调器实例 * @returns {Document} OPF DOM 文档 */ _createContentOpfDocument(bookInfo) { const parser = new DOMParser(); const opfString = ` zh-CN ${cleanXmlIllegalChars(bookInfo.title)}.${cleanXmlIllegalChars(bookInfo.nav_toc[0].volumeName)} urn:uuid:${globalThis.crypto.randomUUID()} ${cleanXmlIllegalChars(bookInfo.creator || "未知作者")} ${`${(/* @__PURE__ */ new Date()).toISOString().split(".")[0]}Z`} ${PROJECT_REPO} ${bookInfo.description ? `${cleanXmlIllegalChars(bookInfo.description)}` : ""} `; const doc = parser.parseFromString(opfString, "text/xml"); const parseError = doc.getElementsByTagName("parsererror"); if (parseError.length > 0) { console.error("解析OPF XML时出错:", parseError[0].textContent); bookInfo.logger.logError("内部错误:创建OPF文档失败。"); throw new Error("Failed to create OPF document due to parser error."); } return doc; }, /** * 填充 metadata 元素 * @param {Element} metadata - metadata DOM 元素 * @param {EpubBuilderCoordinator} bookInfo - 协调器实例 * @param {string} coverImageId - 封面图片的 manifest ID */ _populateMetadata(metadata, bookInfo, coverImageId) { const _metadata = metadata.querySelector("metadata"); if (coverImageId) { const coverMeta = metadata.createElement("meta"); coverMeta.setAttribute("name", "cover"); coverMeta.setAttribute("content", coverImageId); coverMeta.removeAttribute("xmlns"); _metadata.appendChild(coverMeta); } }, /** * 向 manifest 添加 item * @param {Element} manifestElement - manifest DOM 元素 * @param {string} id - item ID * @param {string} href - item 路径 * @param {string} mediaType - item MIME 类型 * @returns {Element} 创建的 item 元素 */ _addManifestItem(manifestElement, id, href, mediaType) { const item = manifestElement.ownerDocument.createElement("item"); item.setAttribute("id", id); item.setAttribute("href", href); item.setAttribute("media-type", mediaType); manifestElement.appendChild(item); return item; }, /** * 向 spine 添加 itemref * @param {Element} spineElement - spine DOM 元素 * @param {string} idref - 关联的 manifest item ID * @param {string} [linear] - 是否线性阅读 * @returns {Element} 创建的 itemref 元素 */ _addSpineItem(spineElement, idref, linear = "yes") { const itemref = spineElement.ownerDocument.createElement("itemref"); itemref.setAttribute("idref", idref); if (linear === "no") { itemref.setAttribute("linear", "no"); } spineElement.appendChild(itemref); return itemref; }, /** * 生成导航文件 (nav.xhtml) 的内容 * @param {Array} navTocEntries - 导航目录数据 (bookInfo.nav_toc) * @returns {string} nav.xhtml 内容字符串 */ _generateNavXhtml(navTocEntries) { const parser = new DOMParser(); const doc = parser.parseFromString(cleanXmlIllegalChars(this.NAV_XHTML_TEMPLATE.content), "application/xhtml+xml"); const tocNavElement = doc.getElementById("toc"); const ol = doc.createElement("ol"); navTocEntries.forEach((volumeToc) => { const vLi = doc.createElement("li"); const vA = doc.createElement("a"); vA.href = volumeToc.volumeHref; vA.textContent = volumeToc.volumeName; vLi.appendChild(vA); if (volumeToc.chapterArr && volumeToc.chapterArr.length > 0) { const cOl = doc.createElement("ol"); volumeToc.chapterArr.forEach((chapterToc) => { const cLi = doc.createElement("li"); const cA = doc.createElement("a"); cA.href = chapterToc.chapterHref; cA.textContent = chapterToc.chapterName; cLi.appendChild(cA); cOl.appendChild(cLi); }); vLi.appendChild(cOl); } ol.appendChild(vLi); }); tocNavElement.appendChild(ol); const serializer = new XMLSerializer(); let xhtmlString = serializer.serializeToString(doc.documentElement); xhtmlString = xhtmlString.replace(/ xmlns=""/g, ""); return ` ${xhtmlString}`; }, /** * 处理并清理分卷HTML内容,插入图片等 * @param {object} textEntry - 当前卷的文本数据 {path, content, id, vid, ...} * @param {EpubBuilderCoordinator} bookInfo - 协调器实例 * @returns {string} 处理后的 XHTML 字符串 */ _processAndCleanVolumeHtml(textEntry, bookInfo) { const parser = new DOMParser(); const initialHtml = ` ${cleanXmlIllegalChars(textEntry.volumeName)}

        ${cleanXmlIllegalChars(textEntry.volumeName)}


        ${textEntry.content} `; const doc = parser.parseFromString(cleanXmlIllegalChars(initialHtml), "text/html"); const body = doc.body; const volumeSpecificLocations = bookInfo.ImgLocation.filter((loc) => loc.vid === textEntry.vid); const volumeImages = bookInfo.Images.filter((img) => img.TextId === textEntry.id || img.smallCover); Array.from(body.querySelectorAll(".txtDropEnable")).forEach((span) => { const spanId = span.id; const childNodes = Array.from(span.childNodes); volumeSpecificLocations.filter((loc) => loc.spanID === spanId).forEach((loc) => { const imgEntry = volumeImages.find((img) => img.idName === loc.imgID && img.content); if (imgEntry) { const divImage = doc.createElement("div"); divImage.setAttribute("class", "divimage"); const imgTag = doc.createElement("img"); imgTag.setAttribute("src", `../${imgEntry.path}`); imgTag.setAttribute("alt", cleanXmlIllegalChars(imgEntry.idName)); imgTag.setAttribute("loading", "lazy"); divImage.appendChild(imgTag); span.parentNode.insertBefore(divImage, span); } else { bookInfo.logger.logWarn(`配置的图片 ${loc.imgID} 在卷 ${textEntry.volumeName} (span: ${loc.spanID}) 未找到或未下载,未插入。`); } }); childNodes.forEach((node) => { span.parentNode.insertBefore(node, span); }); span.parentNode.removeChild(span); }); Array.from(body.getElementsByTagName("p")).forEach((p) => { if (p.innerHTML.trim() === "" || p.innerHTML.trim() === " ") { p.parentNode.removeChild(p); } }); Array.from(body.getElementsByTagName("img")).forEach((img) => { if (!img.closest("div.divimage")) { const wrapper = doc.createElement("div"); wrapper.className = "divimage"; img.parentNode.insertBefore(wrapper, img); wrapper.appendChild(img); } }); const serializer = new XMLSerializer(); let xhtmlString = serializer.serializeToString(doc.documentElement); xhtmlString = xhtmlString.replace(/ xmlns=""/g, ""); return ` ${xhtmlString}`; } }; class EpubBuilderCoordinator { /** * @param {boolean} isEditingMode - 是否进入编辑模式 * @param {boolean} downloadAllVolumes - 是否下载全部分卷 */ constructor(isEditingMode = false, downloadAllVolumes = false) { var _a, _b, _c, _d, _e; this.aid = _unsafeWindow.article_id; this.title = ((_c = (_b = (_a = document.getElementById("title")) == null ? void 0 : _a.childNodes[0]) == null ? void 0 : _b.textContent) == null ? void 0 : _c.trim()) || "未知标题"; this.creator = ((_e = (_d = document.getElementById("info")) == null ? void 0 : _d.textContent) == null ? void 0 : _e.trim()) || "未知作者"; this.description = ""; this.bookUrl = CURRENT_URL.href; this.targetEncoding = _unsafeWindow.targetEncoding; this.nav_toc = []; this.Text = []; this.Images = []; this.ImgLocation = []; this.ePubEidt = isEditingMode; this.ePubEidtDone = false; this.descriptionXhrInitiated = false; this.thumbnailImageAdded = false; this.isDownloadAll = downloadAllVolumes; this.XHRManager = Object.create(XHRDownloadManager); this.XHRManager.init(this); this.logger = UILogger; this.totalTasksAdded = 0; this.tasksCompletedOrSkipped = 0; this.XHRFail = false; this.VOLUME_ID_PREFIX = VOLUME_ID_PREFIX; } /** * 启动下载和构建流程 * @param {HTMLElement} eventTarget - 触发下载的DOM元素 (用于单卷下载时定位) */ start(eventTarget) { this.logger.clearLog(); this.refreshProgress(this, "开始处理书籍..."); this._loadExternalImageConfigs(); const volumeElements = this.isDownloadAll ? Array.from(document.querySelectorAll(".vcss")) : [eventTarget.closest("td.vcss")]; if (volumeElements.some((el) => !el)) { this.logger.logError("未能确定下载目标分卷,请检查页面结构。"); const buildBtn = document.getElementById("EidterBuildBtn"); if (buildBtn) buildBtn.disabled = false; return; } volumeElements.forEach((vcss, index) => { var _a, _b, _c, _d; const volumeName = (_b = (_a = vcss.childNodes[0]) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim(); const volumePageId = vcss.getAttribute("vid"); const firstChapterLink = (_d = (_c = vcss.parentElement) == null ? void 0 : _c.nextElementSibling) == null ? void 0 : _d.querySelector('a[href*=".htm"]'); const firstChapterHref = firstChapterLink == null ? void 0 : firstChapterLink.getAttribute("href"); const firstChapterId = firstChapterHref ? firstChapterHref.split(".")[0] : null; if (!volumeName || !volumePageId || !firstChapterId) { this.logger.logWarn(`分卷 ${index + 1} 信息不完整 (名称: ${volumeName}, 页面ID: ${volumePageId}, 首章ID: ${firstChapterId}),跳过此卷。`); return; } const volumeDomId = `${this.VOLUME_ID_PREFIX}_${index}`; const volumeHref = `${volumeDomId}.xhtml`; const navTocEntry = { volumeName, vid: volumePageId, // 实际的卷标识,用于ImgLocation等 volumeID: volumeDomId, volumeHref, chapterArr: [] // 章节列表稍后填充 }; this.nav_toc.push(navTocEntry); const textEntry = { path: `Text/${volumeHref}`, content: "", // 稍后填充 id: volumeDomId, vid: volumePageId, // 关联到实际卷标识 volumeName, navToc: navTocEntry // 引用,方便处理 }; this.Text.push(textEntry); const downloadUrl = `https://${DOWNLOAD_DOMAIN}/pack.php?aid=${this.aid}&vid=${firstChapterId}`; this.XHRManager.add({ type: "webVolume", // 自定义类型 url: downloadUrl, loadFun: (xhr) => VolumeLoader.loadWebVolumeText(xhr), // 使用VolumeLoader的方法 VolumeIndex: index, // 用于在回调中定位nav_toc和Text中的条目 data: { vid: volumePageId, vcssText: volumeName, Text: textEntry }, // 传递给处理函数的信息 bookInfo: this, // 传递EpubBuilderCoordinator实例 isCritical: true // 卷内容是关键任务 }); }); if (this.Text.length === 0) { this.refreshProgress(this, "没有有效的分卷被添加到下载队列。"); const buildBtn = document.getElementById("EidterBuildBtn"); if (buildBtn) buildBtn.disabled = false; return; } this.tryBuildEpub(); } /** * 通知协调器一个任务已完成 (由 XHRManager 调用) */ handleTaskCompletion() { new Promise((resolve) => { setTimeout(() => { resolve(); }, 1e3); }).then(() => { _unsafeWindow._isUnderConstruction = false; }); } /** * 尝试触发ePub构建流程 * 只有当所有下载任务完成且没有关键失败时才会真正构建 */ tryBuildEpub() { EpubFileBuilder.build(this).then((result) => { if (!result || this.XHRFail) { return; } this.refreshProgress(this, "ePub文件已成功生成。"); this.logger.logInfo("ePub文件已成功生成。", result); const buildBtn = document.getElementById("EidterBuildBtn"); if (buildBtn) buildBtn.disabled = false; }).finally(() => { this.handleTaskCompletion(); }); } /** * 更新进度显示和日志 (适配旧的调用方式) * @param {EpubBuilderCoordinator} instance - 协调器实例 (通常是 this) * @param {string} [message] - 日志消息 */ refreshProgress(instance, message) { this.logger.updateProgress(instance, message); } /** * 从 unsafeWindow.ImgLocationCfgRef 加载外部图片配置 */ _loadExternalImageConfigs() { if (Array.isArray(_unsafeWindow.ImgLocationCfgRef) && _unsafeWindow.ImgLocationCfgRef.length > 0) { let loadedCount = 0; _unsafeWindow.ImgLocationCfgRef.forEach((cfg) => { if (cfg.UID === EPUB_EDITOR_CONFIG_UID && cfg.aid === this.aid && Array.isArray(cfg.ImgLocation) && cfg.ImgLocation.length > 0) { cfg.ImgLocation.forEach((loc) => { if (loc.vid && loc.spanID && loc.imgID) { if (!this.ImgLocation.some( (existing) => existing.vid === loc.vid && existing.spanID === loc.spanID && existing.imgID === loc.imgID )) { this.ImgLocation.push({ vid: String(loc.vid), // 确保类型一致 spanID: String(loc.spanID), imgID: String(loc.imgID) }); loadedCount++; } } }); } }); if (loadedCount > 0) { this.refreshProgress(this, `已加载 ${loadedCount} 条来自书评的插图位置配置。`); } _unsafeWindow.ImgLocationCfgRef = []; } } } function initializeUserScript() { if (typeof _unsafeWindow.OpenCC !== "undefined" && typeof _unsafeWindow.translateButtonId !== "undefined") { OpenCCConverter.init(_unsafeWindow); } else { console.warn("OpenCC或页面翻译环境未准备好,增强的简繁转换未初始化。"); } if (typeof _unsafeWindow.article_id === "undefined") { console.log("非书籍或目录页面,脚本主要功能不激活。"); initializeReviewPageFeatures(); return; } UILogger.init(); if (typeof _unsafeWindow.chapter_id === "undefined" || _unsafeWindow.chapter_id === null || _unsafeWindow.chapter_id === "0") { if (document.querySelector("#title") && document.querySelectorAll(".vcss").length > 0) { addDownloadButtonsToCatalogPage(); } else { console.log("页面缺少chapter_id,且不像是标准目录页,脚本核心功能不激活。"); } } else { handleContentPage(); } initializeReviewPageFeatures(); loadConfigFromUrlIfPresent(); } function addDownloadButtonsToCatalogPage() { const titleElement = document.getElementById("title"); if (!titleElement) return; const bookTitle = titleElement.textContent.trim(); const targetCharset = _unsafeWindow.targetEncoding === "1" ? "big5" : "utf8"; const txtHref = `https://${DOWNLOAD_DOMAIN}/down.php?type=${targetCharset}&id=${_unsafeWindow.article_id}&fname=${encodeURIComponent(bookTitle)}`; const txtTitle = `全本文本下载(${targetCharset})`; const allTxtLink = createTxtDownloadButton(txtTitle, txtHref); titleElement.appendChild(allTxtLink); const epubAllButton = createDownloadButton(" ePub下载(全本)", false, true); const epubAllEditButton = createDownloadButton(" (调整插图)", true, true); titleElement.appendChild(epubAllButton); titleElement.appendChild(epubAllEditButton); const aEleSubEpub = createTxtDownloadButton(" 分卷ePub下载(全本)", "javascript:void(0);", true); aEleSubEpub.className = "DownloadAllSub"; aEleSubEpub.addEventListener("click", (e) => loopDownloadSub()); titleElement.append(aEleSubEpub); document.querySelectorAll("td.vcss").forEach((vcssCell) => { var _a, _b; const volumeName = (_b = (_a = vcssCell.childNodes[0]) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim(); const volumeId = vcssCell.getAttribute("vid"); if (!volumeName || !volumeId) return; const txtHref2 = `https://${DOWNLOAD_DOMAIN}/packtxt.php?aid=${_unsafeWindow.article_id}&vid=${volumeId}&aname=${encodeURIComponent(bookTitle)}&vname=${encodeURIComponent(volumeName)}&charset=${targetCharset.replace("utf8", "utf-8")}`; const txtTitle2 = ` 文本下载(${targetCharset.replace("utf8", "utf-8")})`; const volTxtLink = createTxtDownloadButton(txtTitle2, txtHref2); vcssCell.appendChild(volTxtLink); const epubVolButton = createDownloadButton(" ePub下载(本卷)", false, false); const epubVolEditButton = createDownloadButton(" (调整插图)", true, false); vcssCell.appendChild(epubVolButton); vcssCell.appendChild(epubVolEditButton); }); } function createTxtDownloadButton(title, href, otherType = false) { const button = document.createElement("a"); button.href = href; button.textContent = title; button.style.marginLeft = "5px"; button.style.display = "inline-block"; button.style.padding = "5px 10px"; button.style.textDecoration = "none"; button.style.borderRadius = "3px"; button.style.cursor = "pointer"; button.style.marginLeft = "5px"; button.style.fontSize = "14px"; button.style.lineHeight = "normal"; if (otherType) { button.style.borderColor = "#ffe0b2"; button.style.backgroundColor = "#fff8e1"; button.style.color = "#fb602d"; } else { button.style.borderColor = "#00bcd4"; button.style.backgroundColor = "#b2ebf2"; button.style.color = "#0047a7"; } return button; } function createDownloadButton(text, isEditMode, isDownloadAll) { const button = document.createElement("a"); button.href = "javascript:void(0);"; button.textContent = text; button.style.display = "inline-block"; button.style.padding = "5px 10px"; button.style.border = "1px solid #ccc"; button.style.backgroundColor = "#f0f0f0"; button.style.color = "#333"; button.style.textDecoration = "none"; button.style.borderRadius = "3px"; button.style.cursor = "pointer"; button.style.marginLeft = "5px"; button.style.fontSize = "14px"; button.style.lineHeight = "normal"; if (isEditMode) { button.style.borderColor = "#ff9800"; button.style.backgroundColor = "#fff3e0"; button.style.color = "#e65100"; button.className = ""; button.classList.add("EditMode"); } else if (isDownloadAll) { button.style.borderColor = "#4caf50"; button.style.backgroundColor = "#e8f5e9"; button.style.color = "#1b5e20"; button.className = ""; button.classList.add("DownloadAll"); } else { button.className = "ePubSub"; } button.addEventListener("click", (event) => { event.target.style.pointerEvents = "none"; event.target.style.opacity = "0.6"; event.target.style.color = "#aaa"; const coordinator = new EpubBuilderCoordinator(isEditMode, isDownloadAll); coordinator.start(event.target); }); return button; } function loopDownloadSub() { const elements = document.querySelectorAll("a.ePubSub"); const linksArray = Array.from(elements); const delayBetweenClicks = 5e3; const checkInterval = 5e3; const constructionTimeout = 6e4; UILogger.logInfo("循环下载分卷ePub(全本)..."); function processElement(index) { if (index >= linksArray.length) { UILogger.logInfo("所有分卷下载任务处理完毕。"); return; } const currentLink = linksArray[index]; UILogger.logInfo(`开始处理链接: ${currentLink.href} (索引: ${index})`); checkConstructionStatus(index, Date.now()); } function checkConstructionStatus(index, startTime) { const currentTime = Date.now(); const elapsedTime = currentTime - startTime; const isUnderConstruction = typeof _unsafeWindow !== "undefined" && _unsafeWindow._isUnderConstruction; if (isUnderConstruction) { if (elapsedTime > constructionTimeout) { const errorMessage = `处理链接超时(${constructionTimeout / 1e3}s): ${linksArray[index].href} (索引: ${index}),跳过此元素。`; UILogger.logError(errorMessage); processElement(index + 1); } else { UILogger.logInfo(`检测到正在构建状态,已等待 ${elapsedTime / 1e3}s,${checkInterval / 1e3}秒后将再次检查...`); setTimeout(() => checkConstructionStatus(index, startTime), checkInterval); } } else { UILogger.logInfo("未处于构建状态,点击当前链接。"); linksArray[index].click(); UILogger.logInfo(`Clicked link: ${linksArray[index].href}`); setTimeout(() => processElement(index + 1), delayBetweenClicks); } } processElement(0); } function handleContentPage() { const contentMain = document.getElementById("contentmain"); const contentDiv = document.getElementById("content"); if (!contentMain || !contentDiv) return; const isCopyrightRestricted = contentMain.firstElementChild && contentMain.firstElementChild.tagName === "SPAN" && contentMain.firstElementChild.textContent.trim().toLowerCase() === "null"; if (isCopyrightRestricted && typeof _unsafeWindow.chapter_id !== "undefined" && _unsafeWindow.chapter_id !== null && _unsafeWindow.chapter_id !== "0") { UILogger.logInfo("检测到版权限制页面,尝试通过App接口加载内容..."); const bookInfoForReading = { aid: _unsafeWindow.article_id, targetEncoding: _unsafeWindow.targetEncoding, // 传递页面当前目标编码 logger: UILogger, // 传递日志记录器 refreshProgress: (info, msg) => UILogger.updateProgress(info, msg) // 适配旧的refreshProgress }; AppApiService.fetchChapterForReading(bookInfoForReading, _unsafeWindow.chapter_id, contentDiv, _unsafeWindow.translateBody); } } function initializeReviewPageFeatures() { if (!CURRENT_URL.pathname.includes("/modules/article/")) return; document.querySelectorAll(".jieqiCode").forEach((codeElement) => { var _a, _b; const yidAnchor = (_a = codeElement.closest("table")) == null ? void 0 : _a.querySelector('a[name^="y"]'); const yid = yidAnchor == null ? void 0 : yidAnchor.getAttribute("name"); const reviewId = CURRENT_URL.searchParams.get("rid"); const pageNum = CURRENT_URL.searchParams.get("page") || "1"; if (reviewId && yid) { try { const configText = codeElement.textContent; const parsedConfig = JSON.parse(configText.replace(/\s/g, "").replace(/^\uFEFF/, "")); if (parsedConfig && parsedConfig.UID === EPUB_EDITOR_CONFIG_UID && parsedConfig.aid && parsedConfig.pathname && (parsedConfig.ImgLocationBase64 || Array.isArray(parsedConfig.ImgLocation) && parsedConfig.ImgLocation.length > 0)) { const titleDiv = (_b = yidAnchor.closest("tr")) == null ? void 0 : _b.querySelector("td > div"); if (titleDiv) { const useConfigLink = document.createElement("a"); useConfigLink.textContent = "[使用此插图配置生成ePub]"; useConfigLink.style.color = "fuchsia"; useConfigLink.style.marginRight = "10px"; useConfigLink.href = `${CURRENT_URL.origin}${parsedConfig.pathname}?rid=${reviewId}&page=${pageNum}&yid=${yid}&CfgRef=1#title`; titleDiv.insertBefore(useConfigLink, titleDiv.firstChild); } } } catch (e) { } } }); } _unsafeWindow.ImgLocationCfgRef = _unsafeWindow.ImgLocationCfgRef || []; async function loadConfigFromUrlIfPresent() { var _a; const urlParams = CURRENT_URL.searchParams; if (urlParams.get("CfgRef") !== "1") return; const rid = urlParams.get("rid"); const page = urlParams.get("page"); const yidToLoad = urlParams.get("yid"); if (!rid || !yidToLoad) return; const reviewPageUrl = `${CURRENT_URL.origin}/modules/article/reviewshow.php?rid=${rid}&page=${page || 1}`; UILogger.init(); UILogger.logInfo(`尝试从书评页加载插图配置...`); try { const response = await gmXmlHttpRequestAsync({ method: "GET", url: reviewPageUrl, timeout: XHR_TIMEOUT_MS }); if (response.status === 200) { const parser = new DOMParser(); const doc = parser.parseFromString(cleanXmlIllegalChars(response.responseText), "text/html"); const targetAnchor = doc.querySelector(`a[name="${yidToLoad}"]`); const codeElement = (_a = targetAnchor == null ? void 0 : targetAnchor.closest("table")) == null ? void 0 : _a.querySelector(".jieqiCode"); if (codeElement) { const configText = codeElement.textContent; const parsedConfig = JSON.parse(configText.replace(/\s/g, "").replace(/^\uFEFF/, "")); if (parsedConfig.ImgLocationBase64) { try { const zip = new JSZip(); await zip.load(parsedConfig.ImgLocationBase64, { base64: true }); const imgLocFile = zip.file(IMG_LOCATION_FILENAME); if (imgLocFile) { const imgLocJson = await imgLocFile.async("string"); parsedConfig.ImgLocation = JSON.parse(imgLocJson); delete parsedConfig.ImgLocationBase64; } else { throw new Error(`压缩包中未找到 ${IMG_LOCATION_FILENAME} 文件。`); } } catch (zipErr) { throw new Error(`解压或解析配置压缩包失败: ${zipErr.message}`); } } if (parsedConfig && parsedConfig.UID === EPUB_EDITOR_CONFIG_UID && parsedConfig.aid && Array.isArray(parsedConfig.ImgLocation) && parsedConfig.ImgLocation.length > 0) { _unsafeWindow.ImgLocationCfgRef.push(parsedConfig); UILogger.logInfo(`成功加载来自书评的 ${parsedConfig.ImgLocation.length} 条插图位置配置。现在可以点击下载按钮了。`); const titleElem = document.getElementById("title"); if (titleElem) { const notice = document.createElement("p"); notice.style.color = "green"; notice.textContent = `提示:来自书评的插图配置已加载,请点击相应的“ePub下载(调整插图)”按钮开始。`; titleElem.parentNode.insertBefore(notice, titleElem.nextSibling); } } else { UILogger.logError(`书评中的配置无效或不完整。`); } } else { UILogger.logError(`在书评页未能找到对应的配置代码块。`); } } else { UILogger.logError(`下载书评配置失败,状态码: ${response.status}`); } } catch (error) { UILogger.logError(`加载或解析书评配置时出错: ${error.message}`); console.error("加载书评配置错误:", error); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initializeUserScript); } else { initializeUserScript(); } })(JSZip);