WQ

文泉书局

当前为 2024-06-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name WQ
  3. // @namespace http://tampermonkey.net/
  4. // @homepage https://github.com/systemmin/kill-doc
  5. // @version 1.0.2
  6. // @description 文泉书局
  7. // @author Mr.Fang
  8. // @match https://*.wqxuetang.com/deep/read/pdf*
  9. // @require https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/jspdf/2.4.0/jspdf.umd.min.js
  10. // @require https://unpkg.com/@zip.js/zip.js@2.7.34/dist/zip.min.js
  11. // @icon https://dtking.cn/favicon.ico
  12. // @run-at document-idle
  13. // @grant GM_getValue
  14. // @grant GM_deleteValue
  15. // @grant GM_setValue
  16. // @grant GM_download
  17. // @grant GM_notification
  18. // @grant unsafeWindow
  19. // @license Apache-2.0
  20. // ==/UserScript==
  21.  
  22. (function() {
  23. 'use strict';
  24. let MF =
  25. '#MF_fixed{position:fixed;top:50%;transform:translateY(-50%);right:20px;gap:20px;flex-direction:column;z-index:2147483647;display:flex}';
  26. MF +=
  27. '.MF_box{padding:10px;cursor:pointer;border-color:rgb(0,102,255);border-radius:5px;background-color:white;color:rgb(0,102,255);margin-right:10px;box-shadow:rgb(207,207,207) 1px 1px 9px 3px}.MF_active{color: green}#MF_size,#MF_speed{color: red;}';
  28. MF +=
  29. '@media print{html{height:auto !important}body{display:block !important}#app-left{display:none !important}#app-right{display:none !important}#MF_fixed{display:none !important}.menubar{display:none !important}.top-bar-right{display:none !important}.user-guide{display:none !important}#app-reader-editor-below{display:none !important}.no-full-screen{display:none !important}.comp-vip-pop{display:none !important}.center-wrapper{width:auto !important}.reader-thumb,.related-doc-list,.fold-page-content,.try-end-fold-page,.lazy-load,#MF_textarea,#nav-menu-wrap{display:none !important}}'
  30. const prefix = "MF_";
  31. // canvas 禁止重写 drawImage
  32. const canvasRenderingContext2DPrototype = CanvasRenderingContext2D.prototype;
  33. const originalDrawImage = canvasRenderingContext2DPrototype.drawImage;
  34. Object.defineProperty(canvasRenderingContext2DPrototype, 'drawImage', {
  35. value: originalDrawImage,
  36. writable: false,
  37. configurable: false
  38. });
  39.  
  40. class Box {
  41. id = ""; // id
  42. label = ""; // 按钮文本
  43. fun = ""; // 执行方法
  44. constructor(id, label, fun) {
  45. this.id = id;
  46. this.label = label;
  47. this.fun = fun;
  48. }
  49. }
  50.  
  51. class Utility {
  52. debug = true;
  53.  
  54. /**
  55. * 添加 css 样式
  56. * @param e 节点
  57. * @param data JSON 格式样式
  58. */
  59. style(e, data) {
  60. Object.keys(data).forEach(key => {
  61. e.style[key] = data[key]
  62. })
  63. }
  64.  
  65. attr(e, key, val) {
  66. if (!val) {
  67. return e.getAttribute(key);
  68. } else {
  69. e.setAttribute(key, val);
  70. }
  71.  
  72. }
  73.  
  74. /**
  75. * 追加样式
  76. * @param css 格式样式
  77. */
  78. appendStyle(css) {
  79. let style = this.createEl('', 'style');
  80. style.textContent = css;
  81. style.type = 'text/css';
  82. let dom = document.head || document.documentElement;
  83. dom.appendChild(style);
  84. }
  85.  
  86. /**
  87. * @description 创建 dom
  88. * @param id 必填
  89. * @param elType
  90. * @param data
  91. */
  92. createEl(id, elType, data) {
  93. const el = document.createElement(elType);
  94. el.id = id || '';
  95. if (data) {
  96. this.style(el, data);
  97. }
  98. return el;
  99. }
  100.  
  101. query(el) {
  102. return document.querySelector(el);
  103. }
  104.  
  105. queryAll(el) {
  106. return document.querySelectorAll(el);
  107. }
  108.  
  109. update(el, text) {
  110. const elNode = this.query(el);
  111. if (!elNode) {
  112. console.log('节点不存在');
  113. } else {
  114. elNode.innerHTML = text;
  115. }
  116. }
  117.  
  118. /**
  119. * 进度
  120. * @param current 当前数量 -1预览结束
  121. * @param total 总数量
  122. * @param content 内容
  123. */
  124. preview(current, total, content) {
  125. return new Promise(async (resolve, reject) => {
  126. if (current === -1) {
  127. this.update('#' + prefix + 'text', content ? content : "已完成");
  128. } else {
  129. let p = (current / total) * 100;
  130. let ps = p.toFixed(0) > 100 ? 100 : p.toFixed(0);
  131. console.log('当前进度', ps)
  132. this.update('#' + prefix + 'text', '进度' + ps + '%');
  133. await this.sleep(500);
  134. resolve();
  135. }
  136. })
  137.  
  138. }
  139.  
  140. preText(content) {
  141. this.update('#' + prefix + 'text', content);
  142. }
  143.  
  144. gui(boxs) {
  145. const box = this.createEl(prefix + "fixed", 'div');
  146. for (let x in boxs) {
  147. let item = boxs[x];
  148. if (!item.id) continue;
  149. let el = this.createEl(prefix + item.id, 'button');
  150. el.append(new Text(item.label));
  151. if (x === '0') {
  152. el.classList = prefix + 'box ' + prefix + "active";
  153. } else {
  154. el.className = prefix + "box";
  155. }
  156. if (item.fun) {
  157. el.onclick = function() {
  158. eval(item.fun);
  159. }
  160. }
  161. if (item.id === 'speed') {
  162. this.attr(el, 'contenteditable', true)
  163. }
  164. if (item.id === 'size') {
  165. this.attr(el, 'contenteditable', true)
  166. }
  167. box.append(el);
  168. }
  169. document.body.append(box);
  170. }
  171.  
  172. sleep(ms) {
  173. return new Promise(resolve => setTimeout(resolve, ms));
  174. }
  175.  
  176. log(msg) {
  177. if (this.debug) {
  178. console.log(msg);
  179. }
  180. }
  181.  
  182. logt(msg) {
  183. if (this.debug) {
  184. console.table(msg);
  185. }
  186. }
  187. }
  188.  
  189. const u = new Utility();
  190. u.appendStyle(MF);
  191.  
  192.  
  193. const btns = [
  194. new Box('text', '状态 0 %'),
  195. new Box('speed', '1'),
  196. new Box('size', '100'),
  197. new Box('startHandle', '开始执行', 'startHandle()'),
  198. new Box('clearHandle', '结束执行', 'clearHandle()'),
  199. new Box('start', '继续预览', 'autoPreview()'),
  200. new Box('stop', '停止预览', 'stopPreview()'),
  201. new Box('pdf', '下载PDF', 'executeDownload(1)')
  202. ]
  203.  
  204. const domain = {
  205. wqxuetang: 'wqxuetang.com'
  206. };
  207. const {
  208. host,
  209. href,
  210. origin
  211. } = window.location;
  212. const jsPDF = jspdf.jsPDF;
  213. let zipWriter; // 声明全局变量
  214. zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), {
  215. bufferedWrite: true,
  216. useCompressionStream: false
  217. });
  218. const doc = new jsPDF({
  219. orientation: 'p',
  220. unit: 'px',
  221. compress: true
  222. });
  223. // 794 x 1123 px
  224. let pdf_w = 446,
  225. pdf_h = 631,
  226. loading = 500, // 毫秒
  227. pdf_ratio = 0.56,
  228. title = document.title,
  229. fileType = '',
  230. downType = 1, // 下载文件类型
  231. select = null,
  232. selectBox = null,
  233. dom = null,
  234. beforeFun = null,
  235. interval = null,
  236. BASE_URL = 'https://wkretype.bdimg.com/retype',
  237. readerInfoBai = null, // 百度文档参数
  238. intervalBai = null; // 百度定时任务
  239. if (host.includes(domain.taodocs)) {
  240. iscopy = 'TRUE'; // taodocs copy flag
  241. }
  242.  
  243. let size = 0; // 页面容量
  244. let count = 0; // 计数
  245. let times = 0; // 计次
  246.  
  247. const params = new URLSearchParams(document.location.search.substring(1));
  248. if (params.size && params.get('custom')) {
  249. window.parent.postMessage({
  250. type: "onload",
  251. value: 'success'
  252. }, "*")
  253. u.log('子页面加载完成!');
  254. }
  255.  
  256.  
  257. // 监听页面卸载,移除百度定时删除广告等 DOM 定时器
  258. window.onunload = function() {
  259. if (intervalBai) {
  260. clearInterval(intervalBai);
  261. intervalBai = null;
  262. }
  263. }
  264. /**
  265. * @description 前置方法
  266. * @author Mr.Fang
  267. * @time 2024年2月2日
  268. */
  269. const before = () => {
  270. if (beforeFun) {
  271. u.log('---------->beforeFun');
  272. eval(beforeFun)
  273. }
  274. }
  275.  
  276. /**
  277. * @description 初始化方法
  278. * @author Mr.Fang
  279. * @time 2024年2月2日
  280. */
  281. const init = () => {
  282. console.table({
  283. host,
  284. href,
  285. origin
  286. })
  287. dom = document.documentElement || document.body;
  288. if (host.includes(domain.wqxuetang)) {
  289. fileType = "pdf";
  290. select = "#pagebox .page-lmg";
  291. dom = u.query('#scroll');
  292. btns.splice(1, 0, );
  293. }
  294. u.gui(btns);
  295. console.log('文件名称:', title);
  296. console.log('文件类型:', fileType);
  297. }
  298.  
  299.  
  300.  
  301. // load 事件
  302. document.onreadystatechange = function() {
  303. if (document.readyState === "complete") {
  304. console.log('readyState:', document.readyState);
  305. // 在这里执行渲染完成后的操作
  306. console.log('HTML 渲染完成!');
  307. init()
  308. const start = GM_getValue('start');
  309. times = Number(GM_getValue('times')) || 0;
  310. size = Number(GM_getValue('size')) || 0;
  311. if (start) {
  312. console.log('自动开始')
  313. setTimeout(() => {
  314. autoPreview();
  315. console.log('1 ms')
  316. }, 1000)
  317. }
  318. loginfo()
  319. }
  320. };
  321.  
  322. const startHandle = () => {
  323. // 重新设置页面容量参数
  324. if (GM_getValue('size')) {
  325. size = Number(GM_getValue('size'));
  326. } else {
  327. let MF_size = Number(u.query('#MF_size').innerText);
  328. if (MF_size > 0) {
  329. size = MF_size
  330. GM_setValue('size', size)
  331. } else {
  332. u.update('#MF_size', size)
  333. GM_setValue('size', size)
  334. }
  335. }
  336. // 重新设置页码参数
  337. let MF_page = Number(u.query('#MF_speed').innerText) - 1;
  338. if (MF_page > 0) {
  339. GM_setValue('page', MF_page)
  340. localStorage.setItem('WQ_index', MF_page)
  341. }
  342. GM_setValue('start', 1);
  343. console.log('startHandle')
  344. autoPreview();
  345. }
  346.  
  347. const clearHandle = () => {
  348. console.log('clearHandle')
  349. stopPreview();
  350. localStorage.removeItem('start')
  351. localStorage.removeItem('WQ_index')
  352. GM_deleteValue('page')
  353. GM_deleteValue('start')
  354. GM_deleteValue('size')
  355. GM_deleteValue('times')
  356. }
  357.  
  358. const loginfo = () => {
  359. console.log('start', localStorage.getItem('start'))
  360. console.log('WQ_index', localStorage.getItem('WQ_index'))
  361. console.log('GM_page', GM_getValue('page'))
  362. console.log('GM_start', GM_getValue('start'))
  363. console.log('size', size)
  364. console.log('count', count)
  365. console.log('times', times)
  366. }
  367.  
  368. /**
  369. * @description 开始方法,自动预览
  370. * @author Mr.Fang
  371. * @time 2024年2月2日
  372. */
  373. const autoPreview = async () => {
  374. localStorage.setItem('start', '1');
  375. if (GM_getValue('page')) {
  376. localStorage.setItem('WQ_index', GM_getValue('page'))
  377. } else {
  378. let pages = u.query('.page-head-tol').innerText.split('/');
  379. let index = Number(pages[0]) - 1 || 0;
  380. localStorage.setItem('WQ_index', index)
  381. }
  382. await scrollWQxuetang()
  383. return false;
  384. }
  385.  
  386. /**
  387. * @description 结束方法,停止预览
  388. * @author Mr.Fang
  389. * @time 2024年2月2日
  390. */
  391. const stopPreview = async () => {
  392. console.log('---------->stopPreview');
  393. if (interval) {
  394. clearInterval(interval);
  395. interval = null;
  396. }
  397. localStorage.removeItem('start')
  398. }
  399.  
  400. /**
  401. * @description 执行文件下载
  402. * @author Mr.Fang
  403. * @time 2024年2月20日
  404. * @param type 文件类型
  405. */
  406. const executeDownload = async (type) => {
  407. downType = type;
  408. const down = localStorage.getItem('down');
  409. console.log('down', down)
  410. console.log('down', host)
  411. if (!down) {
  412. if (host.includes(domain.wqxuetang)) {
  413. title = u.query('.read-header-title').innerText;
  414. conditionDownload();
  415. }
  416. } else {
  417. conditionDownload();
  418. }
  419. }
  420.  
  421. /**
  422. * 根据指定条件下载文件
  423. */
  424. const conditionDownload = () => {
  425. if (downType === 1) {
  426. downpdf()
  427. localStorage.setItem('down', '1')
  428. } else if (downType === 2) {
  429. downzip()
  430. }
  431. u.preText('下载完成')
  432. }
  433.  
  434.  
  435. /**
  436. * 判断 dom 是否在可视范围内
  437. */
  438. const isElementInViewport = (el) => {
  439. const rect = el.getBoundingClientRect();
  440. return (
  441. rect.top >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight)
  442. );
  443. }
  444. // wq 保存图片
  445. const saveWQImage = async (els, i) => {
  446. let canvas = await MF_ImageJoinToBlob(els);
  447. doc.addPage();
  448. doc.addImage(canvas, 'JPEG', 0, 0, pdf_w, pdf_h, i, 'FAST')
  449. if (doc.internal.pages[1].length === 2) {
  450. doc.deletePage(1); // 删除空白页
  451. }
  452. count++;
  453. localStorage.setItem('WQ_index', i + 1);
  454. GM_setValue('page', i + 1)
  455.  
  456. // 更新dom
  457. u.update('#MF_size', size)
  458. u.update('#MF_speed', i + 1)
  459.  
  460. // 处理分页
  461. if (size === count && count != 0) {
  462. let res = await downpdf();
  463. console.log(res);
  464. GM_setValue('times', times + 1);
  465. await u.sleep(500);
  466. console.log('重载');
  467. window.location.reload()
  468. }
  469. }
  470.  
  471. /**
  472. * wq 边预览边下载
  473. */
  474. const scrollWQxuetang = async () => {
  475. if (!localStorage.getItem("start")) {
  476. u.preview(-1, null, "已终止");
  477. return;
  478. }
  479. if (u.query('.reload_image')) {
  480. console.log('重新加载')
  481. u.query('.reload_image').click();
  482. }
  483. // 判断图片是否加载完成
  484. function isImageLoaded(img) {
  485. return img.complete && img.naturalWidth > 0 && img.naturalHeight > 0;
  486. }
  487.  
  488. function isAllLoaded(childrens) {
  489. if (!childrens.length) {
  490. return false;
  491. }
  492. for (let i = 0; i < childrens.length; i++) {
  493. if (!isImageLoaded(childrens[i])) {
  494. return false;
  495. }
  496. }
  497. return true;
  498. }
  499. let i = Number(localStorage.getItem('WQ_index')) || 0;
  500. let children = u.queryAll(select)
  501. let pages = u.query('.page-head-tol').innerText.split('/');
  502. let index = Number(pages[1]);
  503. if (i === index) {
  504. console.log('执行结束');
  505. u.preview(-1);
  506. clearHandle()
  507. if (size !== count && count != 0) {
  508. let res = await downpdf();
  509. console.log(res);
  510. }
  511. return;
  512. }
  513. let current = children[i];
  514. if (isAllLoaded(current.children)) {
  515. await saveWQImage(current, i)
  516. // 滚动到下一个范围
  517. if (i !== children.length - 1) {
  518. children[i + 1].scrollIntoView({
  519. behavior: "smooth"
  520. });
  521. }
  522. } else {
  523. children[i].scrollIntoView({
  524. behavior: "smooth"
  525. });
  526. }
  527. u.preview(i, children.length);
  528. if (i !== children.length) {
  529. setTimeout(() => {
  530. console.log(loading, 'ms 后执行');
  531. scrollWQxuetang()
  532. }, loading)
  533. }
  534. }
  535.  
  536.  
  537.  
  538.  
  539. /**
  540. * @description 下载压缩包,包含图片
  541. * @author Mr.Fang
  542. * @time 2024年2月2日
  543. */
  544. const downzip = () => {
  545. zipWriter.close().then(blob => {
  546. GM_download(URL.createObjectURL(blob), `${title}.zip`);
  547. URL.revokeObjectURL(blob);
  548.  
  549. // 在关闭旧的 ZipWriter 后,创建新的 ZipWriter
  550. zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), {
  551. bufferedWrite: true,
  552. useCompressionStream: false
  553. });
  554. }).catch(error => {
  555. console.error(error);
  556. });
  557. }
  558.  
  559. /**
  560. * @description 下载 PDF
  561. * @author Mr.Fang
  562. * @time 2024年2月2日
  563. */
  564. const downpdf = async () => {
  565. title = u.query('.read-header-title').innerText;
  566. // 下载 PDF 文件
  567. return doc.save(`${title}_${times}.pdf`, {
  568. returnPromise: true
  569. });
  570. }
  571. // document.querySelector('.reload_image')
  572. // const event = new EventTarget()
  573. // event.dispatchEvent(document.querySelector("#pageImgBox1 > div.page-m-mark"))
  574. // event.onclick()
  575.  
  576. /**
  577. * @description 图片拼接转 blob
  578. * @author Mr.Fang
  579. * @time 2024年6月5日
  580. * @param el 节点对象
  581. * @returns {Promise<blob>}
  582. */
  583. const MF_ImageJoinToBlob = (el) => {
  584. return new Promise((resolve, reject) => {
  585. const children = el.children;
  586. const {
  587. naturalWidth,
  588. naturalHeight
  589. } = children[0];
  590. // 1、创建画布
  591. let canvas = u.createEl('', 'canvas');
  592. canvas.width = naturalWidth * 6;
  593. canvas.height = naturalHeight;
  594. const ctx = canvas.getContext('2d');
  595. // 2、获取所有图片节点
  596. const listData = []
  597. for (var i = 0; i < children.length; i++) {
  598. const img = children[i];
  599. const left = img.style.left.replace('px', '')
  600. listData.push({
  601. index: i,
  602. left: Number(left)
  603. })
  604. }
  605. listData.sort((a, b) => a.left - b.left);
  606. // 3、遍历绘制画布
  607. for (var i = 0; i < listData.length; i++) {
  608. const img = children[listData[i].index];
  609. ctx.drawImage(img, i * naturalWidth, 0, naturalWidth, naturalHeight);
  610. }
  611. resolve(canvas)
  612. })
  613. }
  614.  
  615. /**
  616. * @description 将 blob 对象转 uint8Array
  617. * @author Mr.Fang
  618. * @time 2024年5月27日
  619. * @param {Object} blob 图片对象
  620. * @returns {Promise<Uint8Array>}
  621. */
  622. const MF_BlobToUint8Array = (blob) => {
  623. return new Promise((resolve, reject) => {
  624. const fileReader = new FileReader();
  625. fileReader.onload = function() {
  626. resolve(new Uint8Array(this.result));
  627. };
  628. fileReader.onerror = function(error) {
  629. reject(error);
  630. };
  631. fileReader.readAsArrayBuffer(blob);
  632. });
  633. }
  634.  
  635. /**
  636. * @description 画布输出 blob 对象
  637. * @author Mr.Fang
  638. * @time 2024年1月20日18:05:49
  639. * @param src 图片地址
  640. * @returns {Promise<Object>}
  641. */
  642. const MF_CanvasToBase64 = (canvas) => {
  643. return new Promise((resolve, reject) => {
  644. const {
  645. width,
  646. height
  647. } = canvas;
  648. canvas.toBlob(
  649. (blob) => {
  650. resolve({
  651. blob,
  652. width,
  653. height
  654. });
  655. },
  656. "image/png",
  657. 1,
  658. );
  659. })
  660. }
  661. })();