WQHarvester 文泉收割机

下载文泉书局已购电子书,自动合并阅读器中的书页切片并下载为完整页面图片,需结合仓库里的另一个 Python 脚本使用。

  1. // ==UserScript==
  2. // @name WQHarvester 文泉收割机
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.5
  5. // @description 下载文泉书局已购电子书,自动合并阅读器中的书页切片并下载为完整页面图片,需结合仓库里的另一个 Python 脚本使用。
  6. // @author zetaloop
  7. // @homepage https://github.com/zetaloop/WQHarvester
  8. // @match https://wqbook.wqxuetang.com/deep/read/pdf*
  9. // @match *://wqbook.wqxuetang.com/deep/read/pdf*
  10. // @grant none
  11. // @license The Unlicense
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. "use strict";
  16.  
  17. console.log("文泉收割机已加载");
  18.  
  19. // 跟踪每页的切片加载情况,key为页面index,值为Map {leftValue -> {img, count}}
  20. const pageSlices = {};
  21.  
  22. // 当前处理的最小页面
  23. let currentMinPage = Infinity;
  24.  
  25. // 起始页面
  26. let startPage = 1;
  27.  
  28. // 已完成(合并保存)的页面集合
  29. const completedPages = new Set();
  30.  
  31. // 待合并的页面集合(切片已加载完成但尚未合并)
  32. const pendingPages = new Set();
  33.  
  34. // 处理中的页面集合(等待切片加载中)
  35. const processingPages = new Set();
  36.  
  37. // 当前活动页面(用于控制只合并当前页)
  38. let activePage = null;
  39.  
  40. // 是否正在运行
  41. let isRunning = false;
  42.  
  43. // 脚本是否已初始化
  44. let isInitialized = false;
  45.  
  46. // 是否有面板已创建
  47. let panelCreated = false;
  48.  
  49. // DOM观察器引用
  50. let observer = null;
  51.  
  52. // 页面跳转定时器(确保同时只有一个跳转等待)
  53. let jumpTimeout = null;
  54.  
  55. // 面板各元素引用
  56. let mainPanel,
  57. statusDisplay,
  58. progressDisplay,
  59. currentPageInfo,
  60. mergedProgressDisplay,
  61. completionNotice;
  62.  
  63. // 消息定时器
  64. let noticeTimer = null;
  65.  
  66. // 自动点击“重新加载本页”按钮的定时器
  67. let reloadInterval = null;
  68.  
  69. // 全局合并用的画布,复用以提升效率
  70. let mergeCanvas = null;
  71.  
  72. // 提取书籍ID
  73. function getBookId() {
  74. const urlParams = new URLSearchParams(window.location.search);
  75. return urlParams.get("bid") || "unknown";
  76. }
  77.  
  78. // 新增:保存目录信息为 JSON 文件({bookid}_toc.json)
  79. // JSON 结构:数组,每个节点包含 name(目录名称)、page(对应页码)和 children(子节点数组)
  80. function saveTOC() {
  81. // 查找目录容器(目录区域一般带有 class "catalogue left-scroll")
  82. const tocContainer = document.querySelector(".catalogue.left-scroll");
  83. if (!tocContainer) {
  84. console.log("未找到目录容器,跳过保存目录。");
  85. return;
  86. }
  87. // 在目录容器中查找目录树(一般带有 class "el-tree book-tree")
  88. const treeRoot = tocContainer.querySelector(".el-tree.book-tree");
  89. if (!treeRoot) {
  90. console.log("未找到目录树,跳过保存目录。");
  91. return;
  92. }
  93. // 递归解析目录树
  94. function parseTreeItems(container) {
  95. let items = [];
  96. const treeItems = container.querySelectorAll(
  97. ':scope > [role="treeitem"]'
  98. );
  99. treeItems.forEach((item) => {
  100. let obj = {};
  101. const node = item.querySelector(".tree-node");
  102. if (node) {
  103. const leftSpan = node.querySelector(".node-left");
  104. const rightSpan = node.querySelector(".node-right");
  105. if (leftSpan) {
  106. obj.name = leftSpan.textContent.trim();
  107. }
  108. if (rightSpan) {
  109. const pageSpan = rightSpan.querySelector("span");
  110. if (pageSpan) {
  111. obj.page = pageSpan.textContent.trim();
  112. } else {
  113. obj.page = null;
  114. }
  115. } else {
  116. obj.page = null;
  117. }
  118. }
  119. const childrenGroup = item.querySelector(
  120. ':scope > [role="group"]'
  121. );
  122. if (childrenGroup) {
  123. obj.children = parseTreeItems(childrenGroup);
  124. } else {
  125. obj.children = [];
  126. }
  127. items.push(obj);
  128. });
  129. return items;
  130. }
  131. const toc = parseTreeItems(treeRoot);
  132. const tocJson = JSON.stringify(toc, null, 2);
  133. const bookid = getBookId();
  134. const filename = `${bookid}_toc.json`;
  135. const blob = new Blob([tocJson], { type: "application/json" });
  136. const a = document.createElement("a");
  137. a.href = URL.createObjectURL(blob);
  138. a.download = filename;
  139. a.style.display = "none";
  140. document.body.appendChild(a);
  141. a.click();
  142. setTimeout(() => {
  143. URL.revokeObjectURL(a.href);
  144. document.body.removeChild(a);
  145. }, 100);
  146. console.log(`目录已保存为 ${filename}`);
  147. }
  148.  
  149. // 自动检测并点击“重新加载本页”按钮(每秒检测一次)
  150. function checkReloadButton() {
  151. const reloadButtons = document.querySelectorAll(".reload_image");
  152. reloadButtons.forEach((btn) => {
  153. if (btn.offsetParent !== null) {
  154. // 如果元素可见
  155. const pageBox = btn.closest(".page-img-box");
  156. if (pageBox) {
  157. const pageIndex = pageBox.getAttribute("index");
  158. if (!completedPages.has(pageIndex)) {
  159. console.log(
  160. `检测到页面 ${pageIndex} 的“重新加载本页”按钮,自动点击`
  161. );
  162. updateStatusDisplay(
  163. `检测到页面 ${pageIndex} 重载按钮,正在点击...`
  164. );
  165. btn.click();
  166. }
  167. }
  168. }
  169. });
  170. }
  171.  
  172. // 显示临时通知消息
  173. function showNotice(message, duration = 500) {
  174. if (!completionNotice) return;
  175. if (noticeTimer) clearTimeout(noticeTimer);
  176. completionNotice.textContent = message;
  177. completionNotice.style.opacity = "1";
  178. noticeTimer = setTimeout(() => {
  179. completionNotice.style.opacity = "0";
  180. }, duration);
  181. }
  182.  
  183. // 更新状态信息显示
  184. function updateStatusDisplay(message) {
  185. if (statusDisplay) {
  186. statusDisplay.textContent = message;
  187. }
  188. }
  189.  
  190. // 更新当前页面加载进度显示(针对切片加载进度,显示当前页及加载的切片数量)
  191. function updateCurrentPageInfo(message) {
  192. if (currentPageInfo) {
  193. currentPageInfo.innerHTML = message;
  194. }
  195. }
  196.  
  197. // 更新当前页面的加载进度条(基于切片加载情况,按left区间分6块;颜色根据加载次数)
  198. function updateProgressBar(pageIndex, slices) {
  199. if (!progressDisplay) return;
  200. progressDisplay.innerHTML = "";
  201. if (!slices || slices.size === 0) {
  202. progressDisplay.innerHTML = `
  203. <div class="progress-container">
  204. <div class="progress-item"></div>
  205. <div class="progress-item"></div>
  206. <div class="progress-item"></div>
  207. <div class="progress-item"></div>
  208. <div class="progress-item"></div>
  209. <div class="progress-item"></div>
  210. </div>
  211. `;
  212. return;
  213. }
  214. // 获取所有切片条目,并转换left为数字,保留count信息
  215. const sliceEntries = Array.from(slices.entries()).map(([left, obj]) => [
  216. parseFloat(left),
  217. obj,
  218. ]);
  219. sliceEntries.sort((a, b) => a[0] - b[0]);
  220. const minLeft = sliceEntries[0][0];
  221. const maxLeft = sliceEntries[sliceEntries.length - 1][0];
  222. const range = maxLeft - minLeft;
  223. const interval = range / 5; // 分成6段
  224.  
  225. const container = document.createElement("div");
  226. container.className = "progress-container";
  227.  
  228. // 为每个区间创建一个进度块
  229. for (let i = 0; i < 6; i++) {
  230. const lowerBound =
  231. i === 0 ? minLeft - 0.1 : minLeft + interval * (i - 0.01);
  232. const upperBound =
  233. i === 5 ? maxLeft + 0.1 : minLeft + interval * (i + 1.01);
  234. const progressItem = document.createElement("div");
  235. progressItem.className = "progress-item";
  236.  
  237. // 找到落在该区间的切片,计算最大加载次数
  238. const entriesInInterval = sliceEntries.filter(
  239. ([left, obj]) => left >= lowerBound && left <= upperBound
  240. );
  241. if (entriesInInterval.length > 0) {
  242. const maxCount = Math.max(
  243. ...entriesInInterval.map((e) => e[1].count)
  244. );
  245. // 第一次加载(count==1)使用淡绿色,否则使用深绿色
  246. if (maxCount === 1) {
  247. progressItem.classList.add("loaded-light");
  248. } else {
  249. progressItem.classList.add("loaded-dark");
  250. }
  251. }
  252. container.appendChild(progressItem);
  253. }
  254.  
  255. progressDisplay.appendChild(container);
  256. }
  257.  
  258. // 更新合并进度显示(显示已合并页数 / 总页数)
  259. function updateMergedProgress() {
  260. if (!mergedProgressDisplay) return;
  261. // 将 completedPages 中的所有索引统一转为字符串去重
  262. const mergedIndexes = new Set();
  263. completedPages.forEach((index) => mergedIndexes.add(String(index)));
  264. const totalPages = document.querySelectorAll(".page-img-box").length;
  265. mergedProgressDisplay.textContent = `合并进度:已合并 ${mergedIndexes.size} / ${totalPages} 页`;
  266. }
  267.  
  268. // 获取当前视口中最“可见”的页面索引
  269. function getCurrentVisiblePage() {
  270. const pageElements = document.querySelectorAll(".page-img-box");
  271. if (!pageElements || pageElements.length === 0) return null;
  272. const windowHeight = window.innerHeight;
  273. const scrollTop = window.scrollY || document.documentElement.scrollTop;
  274. let bestVisiblePage = null,
  275. bestVisibility = 0;
  276. pageElements.forEach((page) => {
  277. const rect = page.getBoundingClientRect();
  278. const pageTop = rect.top + scrollTop;
  279. const pageBottom = rect.bottom + scrollTop;
  280. const visibleTop = Math.max(pageTop, scrollTop);
  281. const visibleBottom = Math.min(
  282. pageBottom,
  283. scrollTop + windowHeight
  284. );
  285. const visibleHeight = Math.max(0, visibleBottom - visibleTop);
  286. if (visibleHeight > bestVisibility) {
  287. bestVisibility = visibleHeight;
  288. bestVisiblePage = parseInt(page.getAttribute("index"));
  289. }
  290. });
  291. return bestVisiblePage;
  292. }
  293.  
  294. // 修改后的跳转函数:滚动后立即尝试合并,500ms后检查视口位置,如未到位则重试滚动
  295. function jumpToPage(pageIndex, isRetry = false) {
  296. const pageBox = document.querySelector(
  297. `.page-img-box[index="${pageIndex}"]`
  298. );
  299. if (!pageBox) {
  300. console.log(`找不到第${pageIndex}页元素`);
  301. updateStatusDisplay(`找不到第${pageIndex}页元素`);
  302. return;
  303. }
  304. pageBox.scrollIntoView({ behavior: "smooth", block: "end" });
  305. console.log(`正在跳转第${pageIndex}页${isRetry ? "(重试)" : ""}`);
  306. updateStatusDisplay(`正在跳转第${pageIndex}页...`);
  307.  
  308. // 立即检查:如果该页已标记为待合并,则立刻开始合并
  309. if (pendingPages.has(pageIndex.toString()) && isRunning) {
  310. console.log(`当前活动页面${pageIndex}切片已加载,立即开始合并...`);
  311. mergeAndSavePage(getBookId(), pageIndex.toString());
  312. }
  313.  
  314. // 500ms后检查当前视口是否正确,如有偏差则重试滚动,并强制调用页面完成检测
  315. if (jumpTimeout) clearTimeout(jumpTimeout);
  316. jumpTimeout = setTimeout(() => {
  317. jumpTimeout = null;
  318. const currentPage = getCurrentVisiblePage();
  319. console.log(`跳转后检测: 目标=${pageIndex}, 当前=${currentPage}`);
  320. if (
  321. currentPage !== null &&
  322. Math.abs(currentPage - pageIndex) > 2 &&
  323. !isRetry
  324. ) {
  325. console.log(`跳转偏差过大,再次尝试跳转到第${pageIndex}页`);
  326. jumpToPage(pageIndex, true);
  327. } else {
  328. updateStatusDisplay(`正在转到第${pageIndex}页...`);
  329. activePage = pageIndex;
  330. if (pageSlices[pageIndex]) {
  331. updateProgressBar(pageIndex, pageSlices[pageIndex]);
  332. updateCurrentPageInfo(
  333. `当前页面:<b>第${pageIndex}页</b> (加载切片 ${pageSlices[pageIndex].size} 个)`
  334. );
  335. } else {
  336. updateProgressBar(pageIndex, null);
  337. updateCurrentPageInfo(
  338. `当前页面:<b>第${pageIndex}页</b> (尚未加载切片)`
  339. );
  340. }
  341. // 强制再次检查页面是否已完成加载
  342. checkPageCompletion(getBookId(), pageIndex);
  343. }
  344. }, 500);
  345. }
  346.  
  347. // 处理并记录单个切片图片(同一 left 值如果重复,则累加 count)
  348. function processSliceImage(imgElement, bookId, pageIndex, leftValue) {
  349. if (!isRunning || parseInt(pageIndex) < startPage) return;
  350. if (!pageSlices[pageIndex]) {
  351. pageSlices[pageIndex] = new Map();
  352. processingPages.add(pageIndex);
  353. }
  354. if (pageSlices[pageIndex].has(leftValue)) {
  355. // 重复加载,累加计数
  356. let entry = pageSlices[pageIndex].get(leftValue);
  357. entry.count++;
  358. pageSlices[pageIndex].set(leftValue, entry);
  359. } else {
  360. pageSlices[pageIndex].set(leftValue, { img: imgElement, count: 1 });
  361. }
  362. if (activePage == pageIndex) {
  363. updateProgressBar(pageIndex, pageSlices[pageIndex]);
  364. updateCurrentPageInfo(
  365. `当前页面:<b>第${pageIndex}页</b> (加载切片 ${pageSlices[pageIndex].size} 个)`
  366. );
  367. }
  368. if (
  369. parseInt(pageIndex) < currentMinPage &&
  370. !completedPages.has(pageIndex)
  371. ) {
  372. currentMinPage = parseInt(pageIndex);
  373. jumpToPage(currentMinPage);
  374. }
  375. checkPageCompletion(bookId, pageIndex);
  376. }
  377.  
  378. // 检查页面切片是否全部加载完成
  379. function checkPageCompletion(bookId, pageIndex) {
  380. const pageBox = document.querySelector(
  381. `.page-img-box[index="${pageIndex}"]`
  382. );
  383. if (!pageBox) return;
  384. const plgContainer = pageBox.querySelector(".plg");
  385. if (!plgContainer) return;
  386. const totalSlices = plgContainer.querySelectorAll("img").length;
  387. const currentSlices = pageSlices[pageIndex]
  388. ? pageSlices[pageIndex].size
  389. : 0;
  390. if (
  391. totalSlices > 0 &&
  392. currentSlices >= totalSlices &&
  393. !completedPages.has(pageIndex) &&
  394. !pendingPages.has(pageIndex)
  395. ) {
  396. console.log(`第${pageIndex}页的所有切片已加载,标记为待合并`);
  397. pendingPages.add(pageIndex);
  398. if (activePage == pageIndex && isRunning) {
  399. console.log(`当前活动页面${pageIndex}切片已加载,开始合并...`);
  400. mergeAndSavePage(bookId, pageIndex);
  401. }
  402. }
  403. }
  404.  
  405. // 合并切片并保存为完整页面(保存文件名不带 _complete)——优化点:复用全局画布,优先使用 OffscreenCanvas
  406. function mergeAndSavePage(bookId, pageIndex) {
  407. if (
  408. !pageSlices[pageIndex] ||
  409. pageSlices[pageIndex].size === 0 ||
  410. completedPages.has(pageIndex) ||
  411. !isRunning
  412. )
  413. return;
  414. pendingPages.delete(pageIndex);
  415. updateStatusDisplay(`正在合并第${pageIndex}页...`);
  416. try {
  417. const sortedSlices = Array.from(
  418. pageSlices[pageIndex].entries()
  419. ).sort((a, b) => parseFloat(a[0]) - parseFloat(b[0]));
  420. let totalWidth = 0,
  421. maxHeight = 0;
  422. sortedSlices.forEach(([left, entry]) => {
  423. totalWidth += entry.img.naturalWidth;
  424. maxHeight = Math.max(maxHeight, entry.img.naturalHeight);
  425. });
  426. // 初始化或复用全局画布
  427. if (!mergeCanvas) {
  428. if (typeof OffscreenCanvas !== "undefined") {
  429. mergeCanvas = new OffscreenCanvas(totalWidth, maxHeight);
  430. } else {
  431. mergeCanvas = document.createElement("canvas");
  432. }
  433. }
  434. mergeCanvas.width = totalWidth;
  435. mergeCanvas.height = maxHeight;
  436. const ctx = mergeCanvas.getContext("2d");
  437. let currentX = 0;
  438. sortedSlices.forEach(([left, entry]) => {
  439. ctx.drawImage(entry.img, currentX, 0);
  440. currentX += entry.img.naturalWidth;
  441. });
  442. // 文件名格式:{bookid}_page{pageIndex}.webp
  443. const filename = `${bookId}_page${pageIndex}.webp`;
  444. // 如果使用 OffscreenCanvas 优先使用 convertToBlob
  445. if (
  446. mergeCanvas instanceof OffscreenCanvas &&
  447. mergeCanvas.convertToBlob
  448. ) {
  449. mergeCanvas
  450. .convertToBlob({ type: "image/webp", quality: 0.95 })
  451. .then((blob) => {
  452. saveBlob(blob, filename, pageIndex);
  453. });
  454. } else {
  455. mergeCanvas.toBlob(
  456. function (blob) {
  457. saveBlob(blob, filename, pageIndex);
  458. },
  459. "image/webp",
  460. 0.95
  461. );
  462. }
  463. } catch (error) {
  464. console.error(`合并第${pageIndex}页失败:`, error);
  465. updateStatusDisplay(`合并第${pageIndex}页时出错:${error.message}`);
  466. }
  467. }
  468. // 将生成的 Blob 保存为下载文件
  469. function saveBlob(blob, filename, pageIndex) {
  470. if (!saveBlob.savedFiles) {
  471. saveBlob.savedFiles = new Set();
  472. }
  473. if (saveBlob.savedFiles.has(filename)) {
  474. console.log(`文件 ${filename} 已经保存,跳过重复保存`);
  475. setTimeout(() => {
  476. completedPages.add(pageIndex);
  477. processingPages.delete(pageIndex);
  478. showNotice(`✓ ${pageIndex}页已保存为 ${filename}`);
  479. updateStatusDisplay(`合并完成,继续处理...`);
  480. updateMergedProgress();
  481. console.log("查找下一个未合并页面...");
  482. findAndJumpToNextPage();
  483. }, 100);
  484. return;
  485. }
  486. saveBlob.savedFiles.add(filename);
  487. const link = document.createElement("a");
  488. link.href = URL.createObjectURL(blob);
  489. link.download = filename;
  490. link.style.display = "none";
  491. document.body.appendChild(link);
  492. link.click();
  493. setTimeout(() => {
  494. URL.revokeObjectURL(link.href);
  495. document.body.removeChild(link);
  496. console.log(`已保存合并页面:${filename}`);
  497. completedPages.add(pageIndex);
  498. processingPages.delete(pageIndex);
  499. showNotice(`✓ ${pageIndex}页已保存为 ${filename}`);
  500. updateStatusDisplay(`合并完成,继续处理...`);
  501. updateMergedProgress();
  502. console.log("查找下一个未合并页面...");
  503. findAndJumpToNextPage();
  504. }, 100);
  505. }
  506.  
  507. // 查找并跳转到下一个未合并页面
  508. function findAndJumpToNextPage() {
  509. if (!isRunning) return;
  510. console.log("查找下一个未合并页面...");
  511. const allPages = document.querySelectorAll(".page-img-box");
  512. const allPageIndices = [];
  513. allPages.forEach((page) => {
  514. const idx = parseInt(page.getAttribute("index"));
  515. if (idx >= startPage) allPageIndices.push(idx);
  516. });
  517. allPageIndices.sort((a, b) => a - b);
  518. let nextPage = null;
  519. for (let i = 0; i < allPageIndices.length; i++) {
  520. const pageIdx = allPageIndices[i].toString();
  521. if (
  522. !completedPages.has(pageIdx) &&
  523. parseInt(pageIdx) >= startPage
  524. ) {
  525. nextPage = parseInt(pageIdx);
  526. break;
  527. }
  528. }
  529. if (nextPage !== null) {
  530. currentMinPage = nextPage;
  531. console.log(`跳转到下一未合并页面:${nextPage}`);
  532. jumpToPage(nextPage);
  533. } else {
  534. updateStatusDisplay(`所有页面处理完成!`);
  535. showNotice(`✓ 所有页面处理完成!`, 2000);
  536. const cancelButton = document.getElementById("cancelButton");
  537. const startButton = document.getElementById("startButton");
  538. if (cancelButton) cancelButton.style.display = "none";
  539. if (startButton) {
  540. startButton.disabled = false;
  541. startButton.textContent = "重新开始";
  542. startButton.style.backgroundColor = "#4CAF50";
  543. startButton.style.display = "block";
  544. }
  545. isRunning = false;
  546. }
  547. }
  548.  
  549. // 处理页面中已有的图片
  550. function processExistingImages() {
  551. if (!isRunning) return;
  552. const bookId = getBookId();
  553. console.log(`检测到书籍ID${bookId}`);
  554. document.querySelectorAll(".page-img-box").forEach((pageBox) => {
  555. const pageIndex = pageBox.getAttribute("index");
  556. if (parseInt(pageIndex) < startPage) return;
  557. const plgContainer = pageBox.querySelector(".plg");
  558. if (!plgContainer) return;
  559. const sliceImages = plgContainer.querySelectorAll("img");
  560. sliceImages.forEach((img) => {
  561. if (img.complete && img.naturalHeight !== 0) {
  562. const leftValue = parseFloat(img.style.left) || 0;
  563. processSliceImage(img, bookId, pageIndex, leftValue);
  564. } else {
  565. img.addEventListener("load", function () {
  566. if (!isRunning) return;
  567. const leftValue = parseFloat(img.style.left) || 0;
  568. processSliceImage(img, bookId, pageIndex, leftValue);
  569. });
  570. }
  571. });
  572. });
  573. }
  574.  
  575. // 设置DOM观察器监控新添加的图片
  576. function setupObserver() {
  577. if (observer) {
  578. observer.disconnect();
  579. }
  580. const bookId = getBookId();
  581. observer = new MutationObserver((mutations) => {
  582. if (!isRunning) return;
  583. mutations.forEach((mutation) => {
  584. if (mutation.addedNodes.length) {
  585. mutation.addedNodes.forEach((node) => {
  586. if (
  587. node.nodeName === "IMG" &&
  588. node.parentElement &&
  589. node.parentElement.classList.contains("plg")
  590. ) {
  591. const pageBox = node.closest(".page-img-box");
  592. if (pageBox) {
  593. const pageIndex = pageBox.getAttribute("index");
  594. if (parseInt(pageIndex) < startPage) return;
  595. if (node.complete && node.naturalHeight !== 0) {
  596. const leftValue =
  597. parseFloat(node.style.left) || 0;
  598. processSliceImage(
  599. node,
  600. bookId,
  601. pageIndex,
  602. leftValue
  603. );
  604. } else {
  605. node.addEventListener("load", function () {
  606. if (!isRunning) return;
  607. const leftValue =
  608. parseFloat(node.style.left) || 0;
  609. processSliceImage(
  610. node,
  611. bookId,
  612. pageIndex,
  613. leftValue
  614. );
  615. });
  616. }
  617. }
  618. }
  619. });
  620. }
  621. });
  622. });
  623. const config = {
  624. childList: true,
  625. subtree: true,
  626. attributes: true,
  627. attributeFilter: ["src", "style"],
  628. };
  629. observer.observe(document.body, config);
  630. }
  631.  
  632. // 停止所有处理
  633. function stopProcessing() {
  634. isRunning = false;
  635. if (observer) {
  636. observer.disconnect();
  637. observer = null;
  638. }
  639. updateStatusDisplay("已停止处理");
  640. showNotice("已取消处理", 1000);
  641. const startButton = document.getElementById("startButton");
  642. const cancelButton = document.getElementById("cancelButton");
  643. if (startButton) {
  644. startButton.disabled = false;
  645. startButton.textContent = "重新开始";
  646. startButton.style.backgroundColor = "#4CAF50";
  647. startButton.style.display = "block";
  648. }
  649. if (cancelButton) {
  650. cancelButton.style.display = "none";
  651. }
  652. if (reloadInterval) {
  653. clearInterval(reloadInterval);
  654. reloadInterval = null;
  655. }
  656. }
  657.  
  658. // 添加增强的交互界面(包括进度显示、按钮、以及自动点击重载按钮)
  659. function addEnhancedUI() {
  660. if (panelCreated) return;
  661. const style = document.createElement("style");
  662. style.textContent = `
  663. #wqSlicerPanel {
  664. position: fixed;
  665. top: 100px;
  666. right: 10px;
  667. background-color: rgba(255,255,255,0.97);
  668. color: #333;
  669. padding: 12px;
  670. border-radius: 8px;
  671. z-index: 9999;
  672. width: 300px;
  673. font-family: Arial, sans-serif;
  674. box-shadow: 0 0 15px rgba(0,0,0,0.3);
  675. transition: all 0.3s ease;
  676. }
  677. #wqSlicerPanel .panel-header {
  678. font-weight: bold;
  679. margin-bottom: 12px;
  680. font-size: 15px;
  681. border-bottom: 1px solid #ddd;
  682. padding-bottom: 8px;
  683. display: flex;
  684. justify-content: space-between;
  685. align-items: center;
  686. }
  687. #wqSlicerPanel .panel-section {
  688. margin-bottom: 12px;
  689. padding-bottom: 8px;
  690. border-bottom: 1px solid #eee;
  691. }
  692. #wqSlicerPanel .buttons-container {
  693. display: flex;
  694. justify-content: space-between;
  695. margin-bottom: 10px;
  696. }
  697. #wqSlicerPanel button {
  698. padding: 6px 12px;
  699. border: none;
  700. border-radius: 4px;
  701. cursor: pointer;
  702. font-weight: bold;
  703. transition: all 0.2s;
  704. }
  705. #wqSlicerPanel button:hover { opacity: 0.9; }
  706. #wqSlicerPanel button:active { transform: scale(0.98); }
  707. #startButton { background: #4CAF50; color: white; flex-grow: 1; }
  708. #cancelButton { background: #f44336; color: white; flex-grow: 1; display: none; }
  709. #currentPageInfo { font-size: 13px; margin-bottom: 10px; color: #333; }
  710. #progressDisplay { margin: 10px 0; }
  711. .progress-container {
  712. display: flex;
  713. justify-content: space-between;
  714. height: 12px;
  715. margin: 5px 0;
  716. background: #f0f0f0;
  717. border-radius: 6px;
  718. overflow: hidden;
  719. }
  720. .progress-item {
  721. flex-grow: 1;
  722. height: 100%;
  723. background: #f0f0f0;
  724. margin: 0 1px;
  725. transition: all 0.3s ease;
  726. }
  727. .progress-item.loaded-light { background: #a8d5a2; }
  728. .progress-item.loaded-dark { background: #4CAF50; }
  729. #statusDisplay, #mergedProgressDisplay, #completionNotice { font-size: 13px; color: #555; min-height: 20px; }
  730. #mergedProgressDisplay { margin-top: 5px; }
  731. #completionNotice { color: #4CAF50; margin-top: 8px; font-weight: bold; opacity: 0; transition: opacity 0.5s ease; }
  732. `;
  733. document.head.appendChild(style);
  734.  
  735. const oldPanel = document.getElementById("wqSlicerPanel");
  736. if (oldPanel) oldPanel.remove();
  737.  
  738. const panel = document.createElement("div");
  739. panel.id = "wqSlicerPanel";
  740. panel.innerHTML = `
  741. <div class="panel-header">
  742. <span>文泉收割机</span>
  743. </div>
  744. <div class="panel-section">
  745. <div class="buttons-container">
  746. <button id="startButton">开始处理</button>
  747. <button id="cancelButton">取消处理</button>
  748. </div>
  749. </div>
  750. <div class="panel-section">
  751. <div id="currentPageInfo">当前页面:等待开始...</div>
  752. <div id="progressDisplay"></div>
  753. </div>
  754. <div class="panel-section">
  755. <div id="mergedProgressDisplay">合并进度:0 页</div>
  756. <div id="statusDisplay">点击“开始处理”启动工具</div>
  757. <div id="completionNotice"></div>
  758. </div>
  759. `;
  760. document.body.appendChild(panel);
  761. panelCreated = true;
  762.  
  763. mainPanel = panel;
  764. statusDisplay = document.getElementById("statusDisplay");
  765. progressDisplay = document.getElementById("progressDisplay");
  766. currentPageInfo = document.getElementById("currentPageInfo");
  767. mergedProgressDisplay = document.getElementById(
  768. "mergedProgressDisplay"
  769. );
  770. completionNotice = document.getElementById("completionNotice");
  771.  
  772. const startButton = document.getElementById("startButton");
  773. startButton.addEventListener("click", function () {
  774. // 首先尝试点击目录区域的 <small> 标签展开全部目录
  775. const expandSmall = document.querySelector(
  776. ".catalogue.left-scroll small"
  777. );
  778. if (expandSmall) {
  779. expandSmall.click();
  780. console.log("点击展开全部目录");
  781. }
  782. // 等待100ms后保存目录
  783. setTimeout(() => {
  784. saveTOC();
  785. showNotice("✓ 目录已保存");
  786. // 再等待100ms后继续原有流程
  787. setTimeout(() => {
  788. if (!isInitialized || !isRunning) {
  789. startButton.disabled = true;
  790. startButton.textContent = "处理中...";
  791. startButton.style.backgroundColor = "#888";
  792. startButton.style.display = "none";
  793. const cancelButton =
  794. document.getElementById("cancelButton");
  795. if (cancelButton) cancelButton.style.display = "block";
  796. if (isInitialized) {
  797. isRunning = true;
  798. initScript(false);
  799. } else {
  800. isRunning = true;
  801. initScript(true);
  802. }
  803. // 启动自动点击重载按钮的检测,每秒执行一次
  804. if (!reloadInterval) {
  805. reloadInterval = setInterval(
  806. checkReloadButton,
  807. 1000
  808. );
  809. }
  810. }
  811. }, 100);
  812. }, 100);
  813. });
  814.  
  815. document
  816. .getElementById("cancelButton")
  817. .addEventListener("click", function () {
  818. stopProcessing();
  819. });
  820.  
  821. updateProgressBar(null, null);
  822. updateMergedProgress();
  823. }
  824.  
  825. // 初始化脚本,询问起始页面
  826. function initScript(isFirstTime = true) {
  827. if (isFirstTime) {
  828. currentMinPage = Infinity;
  829. pendingPages.clear();
  830. processingPages.clear();
  831. activePage = null;
  832. const userStartPage = prompt("请输入要开始处理的页码:");
  833. // 如果用户取消或未输入页码,则取消操作并恢复开始按钮状态
  834. if (!userStartPage) {
  835. stopProcessing();
  836. const startButton = document.getElementById("startButton");
  837. if (startButton) {
  838. startButton.disabled = false;
  839. startButton.textContent = "开始处理";
  840. startButton.style.backgroundColor = "#4CAF50";
  841. startButton.style.display = "block";
  842. }
  843. updateStatusDisplay("操作已取消");
  844. return;
  845. }
  846. // 如果输入的内容不是有效数字,则同样取消操作
  847. if (isNaN(parseInt(userStartPage))) {
  848. stopProcessing();
  849. const startButton = document.getElementById("startButton");
  850. if (startButton) {
  851. startButton.disabled = false;
  852. startButton.textContent = "开始处理";
  853. startButton.style.backgroundColor = "#4CAF50";
  854. startButton.style.display = "block";
  855. }
  856. updateStatusDisplay("无效的页码,操作已取消");
  857. return;
  858. }
  859. startPage = parseInt(userStartPage);
  860. currentMinPage = startPage;
  861. jumpToPage(currentMinPage);
  862. console.log(`开始处理,起始页为:${startPage}`);
  863. updateStatusDisplay(`开始处理,起始页:第${startPage}页`);
  864. } else {
  865. findAndJumpToNextPage();
  866. }
  867. processExistingImages();
  868. setupObserver();
  869. isInitialized = true;
  870. }
  871.  
  872. // 页面加载完成后执行
  873. window.addEventListener("load", function () {
  874. console.log("页面已加载,添加交互界面");
  875. addEnhancedUI();
  876. });
  877.  
  878. // 尝试立即添加交互界面
  879. setTimeout(addEnhancedUI, 500);
  880. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址