WQ

文泉书局

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         WQ
// @namespace    http://tampermonkey.net/
// @homepage	 https://github.com/systemmin/kill-doc
// @version      1.0.2
// @description  文泉书局
// @author       Mr.Fang
// @match        https://*.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/[email protected]/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
// ==/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: '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<blob>}
	 */
	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<Uint8Array>}
	 */
	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<Object>}
	 */
	const MF_CanvasToBase64 = (canvas) => {
		return new Promise((resolve, reject) => {
			const {
				width,
				height
			} = canvas;
			canvas.toBlob(
				(blob) => {
					resolve({
						blob,
						width,
						height
					});
				},
				"image/png",
				1,
			);
		})
	}
})();