WQ

文泉书局

目前為 2024-06-23 提交的版本,檢視 最新版本

// ==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,
			);
		})
	}
})();