// ==UserScript== // @name WQ // @namespace http://tampermonkey.net/ // @homepage https://github.com/systemmin/kill-doc // @version 1.0.1 // @description 文泉电子书分段下载 // @author Mr.Fang // @match https://wqbook.wqxuetang.com/deep/read/pdf* // @require https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/jspdf/2.4.0/jspdf.umd.min.js // @require https://unpkg.com/@zip.js/zip.js@2.7.34/dist/zip.min.js // @icon https://dtking.cn/favicon.ico // @run-at document-idle // @grant GM_getValue // @grant GM_deleteValue // @grant GM_setValue // @grant GM_download // @grant GM_notification // @grant unsafeWindow // @license Apache-2.0 // @downloadURL none // ==/UserScript== (function() { 'use strict'; let MF = '#MF_fixed{position:fixed;top:50%;transform:translateY(-50%);right:20px;gap:20px;flex-direction:column;z-index:2147483647;display:flex}'; MF += '.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;}'; MF += '@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}}' const prefix = "MF_"; // canvas 禁止重写 drawImage const canvasRenderingContext2DPrototype = CanvasRenderingContext2D.prototype; const originalDrawImage = canvasRenderingContext2DPrototype.drawImage; Object.defineProperty(canvasRenderingContext2DPrototype, 'drawImage', { value: originalDrawImage, writable: false, configurable: false }); class Box { id = ""; // id label = ""; // 按钮文本 fun = ""; // 执行方法 constructor(id, label, fun) { this.id = id; this.label = label; this.fun = fun; } } class Utility { debug = true; /** * 添加 css 样式 * @param e 节点 * @param data JSON 格式样式 */ style(e, data) { Object.keys(data).forEach(key => { e.style[key] = data[key] }) } attr(e, key, val) { if (!val) { return e.getAttribute(key); } else { e.setAttribute(key, val); } } /** * 追加样式 * @param css 格式样式 */ appendStyle(css) { let style = this.createEl('', 'style'); style.textContent = css; style.type = 'text/css'; let dom = document.head || document.documentElement; dom.appendChild(style); } /** * @description 创建 dom * @param id 必填 * @param elType * @param data */ createEl(id, elType, data) { const el = document.createElement(elType); el.id = id || ''; if (data) { this.style(el, data); } return el; } query(el) { return document.querySelector(el); } queryAll(el) { return document.querySelectorAll(el); } update(el, text) { const elNode = this.query(el); if (!elNode) { console.log('节点不存在'); } else { elNode.innerHTML = text; } } /** * 进度 * @param current 当前数量 -1预览结束 * @param total 总数量 * @param content 内容 */ preview(current, total, content) { return new Promise(async (resolve, reject) => { if (current === -1) { this.update('#' + prefix + 'text', content ? content : "已完成"); } else { let p = (current / total) * 100; let ps = p.toFixed(0) > 100 ? 100 : p.toFixed(0); console.log('当前进度', ps) this.update('#' + prefix + 'text', '进度' + ps + '%'); await this.sleep(500); resolve(); } }) } preText(content) { this.update('#' + prefix + 'text', content); } gui(boxs) { const box = this.createEl(prefix + "fixed", 'div'); for (let x in boxs) { let item = boxs[x]; if (!item.id) continue; let el = this.createEl(prefix + item.id, 'button'); el.append(new Text(item.label)); if (x === '0') { el.classList = prefix + 'box ' + prefix + "active"; } else { el.className = prefix + "box"; } if (item.fun) { el.onclick = function() { eval(item.fun); } } if (item.id === 'speed') { this.attr(el, 'contenteditable', true) } if (item.id === 'size') { this.attr(el, 'contenteditable', true) } box.append(el); } document.body.append(box); } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } log(msg) { if (this.debug) { console.log(msg); } } logt(msg) { if (this.debug) { console.table(msg); } } } const u = new Utility(); u.appendStyle(MF); const btns = [ new Box('text', '状态 0 %'), new Box('speed', '1'), new Box('size', '100'), new Box('startHandle', '开始执行', 'startHandle()'), new Box('clearHandle', '结束执行', 'clearHandle()'), new Box('start', '继续预览', 'autoPreview()'), new Box('stop', '停止预览', 'stopPreview()'), new Box('pdf', '下载PDF', 'executeDownload(1)') ] const domain = { wqxuetang: 'wqbook.wqxuetang.com' }; const { host, href, origin } = window.location; const jsPDF = jspdf.jsPDF; let zipWriter; // 声明全局变量 zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), { bufferedWrite: true, useCompressionStream: false }); const doc = new jsPDF({ orientation: 'p', unit: 'px', compress: true }); // 794 x 1123 px let pdf_w = 446, pdf_h = 631, loading = 500, // 毫秒 pdf_ratio = 0.56, title = document.title, fileType = '', downType = 1, // 下载文件类型 select = null, selectBox = null, dom = null, beforeFun = null, interval = null, BASE_URL = 'https://wkretype.bdimg.com/retype', readerInfoBai = null, // 百度文档参数 intervalBai = null; // 百度定时任务 if (host.includes(domain.taodocs)) { iscopy = 'TRUE'; // taodocs copy flag } let size = 0; // 页面容量 let count = 0; // 计数 let times = 0; // 计次 const params = new URLSearchParams(document.location.search.substring(1)); if (params.size && params.get('custom')) { window.parent.postMessage({ type: "onload", value: 'success' }, "*") u.log('子页面加载完成!'); } // 监听页面卸载,移除百度定时删除广告等 DOM 定时器 window.onunload = function() { if (intervalBai) { clearInterval(intervalBai); intervalBai = null; } } /** * @description 前置方法 * @author Mr.Fang * @time 2024年2月2日 */ const before = () => { if (beforeFun) { u.log('---------->beforeFun'); eval(beforeFun) } } /** * @description 初始化方法 * @author Mr.Fang * @time 2024年2月2日 */ const init = () => { console.table({ host, href, origin }) dom = document.documentElement || document.body; if (host.includes(domain.wqxuetang)) { fileType = "pdf"; select = "#pagebox .page-lmg"; dom = u.query('#scroll'); btns.splice(1, 0, ); } u.gui(btns); console.log('文件名称:', title); console.log('文件类型:', fileType); } // load 事件 document.onreadystatechange = function() { if (document.readyState === "complete") { console.log('readyState:', document.readyState); // 在这里执行渲染完成后的操作 console.log('HTML 渲染完成!'); init() const start = GM_getValue('start'); times = Number(GM_getValue('times')) || 0; size = Number(GM_getValue('size')) || 0; if (start) { console.log('自动开始') setTimeout(() => { autoPreview(); console.log('1 ms') }, 1000) } loginfo() } }; const startHandle = () => { // 重新设置页面容量参数 if (GM_getValue('size')) { size = Number(GM_getValue('size')); } else { let MF_size = Number(u.query('#MF_size').innerText); if (MF_size > 0) { size = MF_size GM_setValue('size', size) } else { u.update('#MF_size', size) GM_setValue('size', size) } } // 重新设置页码参数 let MF_page = Number(u.query('#MF_speed').innerText) - 1; if (MF_page > 0) { GM_setValue('page', MF_page) localStorage.setItem('WQ_index', MF_page) } GM_setValue('start', 1); console.log('startHandle') autoPreview(); } const clearHandle = () => { console.log('clearHandle') stopPreview(); localStorage.removeItem('start') localStorage.removeItem('WQ_index') GM_deleteValue('page') GM_deleteValue('start') GM_deleteValue('size') GM_deleteValue('times') } const loginfo = () => { console.log('start', localStorage.getItem('start')) console.log('WQ_index', localStorage.getItem('WQ_index')) console.log('GM_page', GM_getValue('page')) console.log('GM_start', GM_getValue('start')) console.log('size', size) console.log('count', count) console.log('times', times) } /** * @description 开始方法,自动预览 * @author Mr.Fang * @time 2024年2月2日 */ const autoPreview = async () => { localStorage.setItem('start', '1'); if (GM_getValue('page')) { localStorage.setItem('WQ_index', GM_getValue('page')) } else { let pages = u.query('.page-head-tol').innerText.split('/'); let index = Number(pages[0]) - 1 || 0; localStorage.setItem('WQ_index', index) } await scrollWQxuetang() return false; } /** * @description 结束方法,停止预览 * @author Mr.Fang * @time 2024年2月2日 */ const stopPreview = async () => { console.log('---------->stopPreview'); if (interval) { clearInterval(interval); interval = null; } localStorage.removeItem('start') } /** * @description 执行文件下载 * @author Mr.Fang * @time 2024年2月20日 * @param type 文件类型 */ const executeDownload = async (type) => { downType = type; const down = localStorage.getItem('down'); console.log('down', down) console.log('down', host) if (!down) { if (host.includes(domain.wqxuetang)) { title = u.query('.read-header-title').innerText; conditionDownload(); } } else { conditionDownload(); } } /** * 根据指定条件下载文件 */ const conditionDownload = () => { if (downType === 1) { downpdf() localStorage.setItem('down', '1') } else if (downType === 2) { downzip() } u.preText('下载完成') } /** * 判断 dom 是否在可视范围内 */ const isElementInViewport = (el) => { const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight) ); } // wq 保存图片 const saveWQImage = async (els, i) => { let canvas = await MF_ImageJoinToBlob(els); doc.addPage(); doc.addImage(canvas, 'JPEG', 0, 0, pdf_w, pdf_h, i, 'FAST') if (doc.internal.pages[1].length === 2) { doc.deletePage(1); // 删除空白页 } count++; localStorage.setItem('WQ_index', i + 1); GM_setValue('page', i + 1) // 更新dom u.update('#MF_size', size) u.update('#MF_speed', i + 1) // 处理分页 if (size === count && count != 0) { let res = await downpdf(); console.log(res); GM_setValue('times', times + 1); await u.sleep(500); console.log('重载'); window.location.reload() } } /** * wq 边预览边下载 */ const scrollWQxuetang = async () => { if (!localStorage.getItem("start")) { u.preview(-1, null, "已终止"); return; } if (u.query('.reload_image')) { console.log('重新加载') u.query('.reload_image').click(); } // 判断图片是否加载完成 function isImageLoaded(img) { return img.complete && img.naturalWidth > 0 && img.naturalHeight > 0; } function isAllLoaded(childrens) { if (!childrens.length) { return false; } for (let i = 0; i < childrens.length; i++) { if (!isImageLoaded(childrens[i])) { return false; } } return true; } let i = Number(localStorage.getItem('WQ_index')) || 0; let children = u.queryAll(select) let pages = u.query('.page-head-tol').innerText.split('/'); let index = Number(pages[1]); if (i === index) { console.log('执行结束'); u.preview(-1); clearHandle() if (size !== count && count != 0) { let res = await downpdf(); console.log(res); } return; } let current = children[i]; if (isAllLoaded(current.children)) { await saveWQImage(current, i) // 滚动到下一个范围 if (i !== children.length - 1) { children[i + 1].scrollIntoView({ behavior: "smooth" }); } } else { children[i].scrollIntoView({ behavior: "smooth" }); } u.preview(i, children.length); if (i !== children.length) { setTimeout(() => { console.log(loading, 'ms 后执行'); scrollWQxuetang() }, loading) } } /** * @description 下载压缩包,包含图片 * @author Mr.Fang * @time 2024年2月2日 */ const downzip = () => { zipWriter.close().then(blob => { GM_download(URL.createObjectURL(blob), `${title}.zip`); URL.revokeObjectURL(blob); // 在关闭旧的 ZipWriter 后,创建新的 ZipWriter zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), { bufferedWrite: true, useCompressionStream: false }); }).catch(error => { console.error(error); }); } /** * @description 下载 PDF * @author Mr.Fang * @time 2024年2月2日 */ const downpdf = async () => { title = u.query('.read-header-title').innerText; // 下载 PDF 文件 return doc.save(`${title}_${times}.pdf`, { returnPromise: true }); } // document.querySelector('.reload_image') // const event = new EventTarget() // event.dispatchEvent(document.querySelector("#pageImgBox1 > div.page-m-mark")) // event.onclick() /** * @description 图片拼接转 blob * @author Mr.Fang * @time 2024年6月5日 * @param el 节点对象 * @returns {Promise} */ const MF_ImageJoinToBlob = (el) => { return new Promise((resolve, reject) => { const children = el.children; const { naturalWidth, naturalHeight } = children[0]; // 1、创建画布 let canvas = u.createEl('', 'canvas'); canvas.width = naturalWidth * 6; canvas.height = naturalHeight; const ctx = canvas.getContext('2d'); // 2、获取所有图片节点 const listData = [] for (var i = 0; i < children.length; i++) { const img = children[i]; const left = img.style.left.replace('px', '') listData.push({ index: i, left: Number(left) }) } listData.sort((a, b) => a.left - b.left); // 3、遍历绘制画布 for (var i = 0; i < listData.length; i++) { const img = children[listData[i].index]; ctx.drawImage(img, i * naturalWidth, 0, naturalWidth, naturalHeight); } resolve(canvas) }) } /** * @description 将 blob 对象转 uint8Array * @author Mr.Fang * @time 2024年5月27日 * @param {Object} blob 图片对象 * @returns {Promise} */ const MF_BlobToUint8Array = (blob) => { return new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = function() { resolve(new Uint8Array(this.result)); }; fileReader.onerror = function(error) { reject(error); }; fileReader.readAsArrayBuffer(blob); }); } /** * @description 画布输出 blob 对象 * @author Mr.Fang * @time 2024年1月20日18:05:49 * @param src 图片地址 * @returns {Promise} */ const MF_CanvasToBase64 = (canvas) => { return new Promise((resolve, reject) => { const { width, height } = canvas; canvas.toBlob( (blob) => { resolve({ blob, width, height }); }, "image/png", 1, ); }) } })();