// ==UserScript== // @name 极简小说阅读模式 // @namespace http://tampermonkey.net/ // @version 0.6.2 // @description 极简移动端小说阅读模式,自动加载下一章 // @author You // @run-at document-start // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @match *://*/* // ==/UserScript== (function() { 'use strict'; // 2. 创建纯净的HTML结构 const cleanHTML = ` 纯净视图
加载中...
`; // 获取DOM元素 // 当前章节信息 let currentChapter = { title: '', content: '', url: window.location.href, nextUrl: '' }; let getNexturlByClick = false; const cacheChapter = []; // 加载状态 let isLoading = false; function getTextLength(element) { let length = 0; const treeWalker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const parent = node.parentElement; if (!parent || parent.closest('script, style, noscript, link, meta, a')) { return NodeFilter.FILTER_REJECT; } return node.textContent.trim().length > 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; } } ); while (treeWalker.nextNode()) { length += treeWalker.currentNode.textContent.length; } return length; } // 提取页面内容 function extractContent(document) { const content = Array.from(document.body.children) .filter(child => !child.className.includes('foot')) .map(child => ({ element: child, textLength: getTextLength(child) })) .reduce((max, current) => current.textLength > max.textLength ? current : max ).element; const testContent = content.cloneNode(true); remove(testContent) if(testContent.textContent.replace(/\s/g, '').length < 50) throw new Error('正文长度不足,尝试使用iframe获取'); // 获取下一章链接 const links = document.querySelectorAll('a'); const nextKeywords = ['下一章','下章','下—章', '下一页','下页','下—页', '下一节','下节']; let nextBtn = Array.from(links).find(a => nextKeywords.includes(a.textContent.trim()) && !a.href.includes('javascript')); const isNextLink = (a) => { const className = a.className.toLowerCase(); const id = a.id.toLowerCase(); const text = a.textContent.toLowerCase(); const href = a.href.toLowerCase(); // 规则1:类名或 ID 包含 "next" if (className.includes('next') || id.includes('next')) return true; // 规则2:父元素/祖先元素类名或 ID 包含 "next" if (a.closest('[class*="next"], [id*="next"]')) return true; // 规则3:子元素(图标)包含 "next"(如 ) if (a.querySelector('i[class*="next"],i[class*="right"], span[class*="next"],span[class*="right"]')) return true; // 规则4:链接文本或 href 包含 "next"(备用) if (text.includes('next') || href.includes('next')) return true; return false; }; if(!nextBtn){ const allHtmlLinks = Array.from(links).filter(isNextLink); nextBtn = allHtmlLinks.pop(); } if(nextBtn && !nextBtn.href){ const observer = new MutationObserver(() => { if(nextBtn.href) { currentChapter.nextUrl = nextBtn.href observer.disconnect(); remove(currentChapter.content); iframe?.remove(); } }); // 开始监听 observer.observe(nextBtn, { attributes: true, // 监听属性变化 attributeFilter: ['href'] // 只监听 href }); } function remove(content){ // 要移除的标签及其内容 const removeTags = ['SCRIPT', 'STYLE', 'A', 'BUTTON', 'IMG', 'IFRAME', 'FORM', 'INPUT', 'NAV', 'HEADER', 'FOOTER', 'AD', 'INS', 'SVG', 'VIDEO', 'AUDIO', 'UL', 'DL']; // 1. 移除不需要的P标签 - 使用remove()方法 removeTags.forEach(tag => { const elements = content.getElementsByTagName(tag); for (let i = elements.length - 1; i >= 0; i--) { elements[i].remove(); // 替换 parentNode.removeChild(elements[i]) } }); const ting = content.querySelectorAll('div[class*="ting"]'); Array.from(ting).forEach(dom => dom.remove()); const set = content.querySelectorAll('div[class*="set"]'); Array.from(set).forEach(dom => dom.remove()); } //没有找到a标签 if(!nextBtn){ const allElements = Array.from(document.querySelectorAll('*')); nextBtn = nextBtn || allElements.find(element => nextKeywords.includes(element.textContent) ); if(nextBtn?.onclick){ const fn = nextBtn.onclick.toString(); const fnContent = fn.match(/{([\s\S]*)}/m)[1].trim(); getNexturlByClick = true; const newFn = new Function(` with(this){ ${fnContent}}`).call({}); remove(currentChapter.content); iframe?.remove(); } } if(nextBtn && nextBtn.href.startsWith('javascript') && nextBtn.getAttribute('data-href')){ nextBtn.href = nextBtn.getAttribute('data-href'); } if(!nextBtn){ const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if(nextKeywords.includes(mutation.target.textContent)){ if(mutation.target.onclick){ const fn = mutation.target.onclick.toString(); const fnContent = fn.match(/{([\s\S]*)}/m)[1].trim(); getNexturlByClick = true; const newFn = new Function(` with(this){ ${fnContent}}`).call({}); observer.disconnect(); remove(currentChapter.content); iframe?.remove(); } } }); }); observer.observe(document.body, { childList: true, subtree: true, }); } if(nextBtn?.href){ remove(content); iframe?.remove(); } if(document.title && !document.querySelector('h1')){ const h3 = document.createElement('h3'); h3.innerHTML = document.title; content.insertBefore(h3, content.firstElementChild); } return { content: content.cloneNode(true) || '

未能提取正文内容

', url: window.location.href, nextUrl: nextBtn?.href }; } let iframe; function extractContentByIframe(srcDoc, time){ // 2. 创建 iframe 沙盒 return new Promise((resolve, reject) => { iframe = document._createElement('iframe'); iframe.style.display = 'none'; iframe.name = 'sandbox-iframe'; iframe.sandbox = 'allow-same-origin allow-scripts'; document.body.appendChild(iframe); iframe.onload = function() { console.log('iframe加载完毕'); resolve(extractContent(iframe.contentDocument)) } iframe.srcdoc = srcDoc; }); } function extractContentByIframe2(url){ // 2. 创建 iframe 沙盒 return new Promise((resolve, reject) => { let iframe = document._createElement('iframe'); iframe.style.display = 'none'; iframe.name = 'sandbox-iframe'; iframe.sandbox = 'allow-same-origin allow-scripts'; document.body.appendChild(iframe); iframe.onload = function() { let debounceTimer; const observer = new MutationObserver((mutationsList) => { // 清除未执行的定时器,确保只保留最后一次 clearTimeout(debounceTimer); // 延迟执行,合并变动 debounceTimer = setTimeout(() => { observer.disconnect(); resolve(extractContent(iframe.contentDocument)) setTimeout(() => iframe.remove()); // 在此执行业务逻辑(如保存、渲染等) }, 300); // 延迟时间根据需求调整(单位:毫秒) }); observer.observe(iframe.contentDocument.body, { childList: true, subtree: true, characterData: true }); } iframe.src = url; }); } // 加载章节内容 async function loadChapter(url) { if (isLoading) return; isLoading = true; const loadingEl = document.getElementById('simple-reader-loading') loadingEl.style.display = 'block'; try { //const response = await fetch(url, { headers:{ "Content-Type": "text/html" } }); function GM_xhr(options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...options, onload: (response) => resolve(response), onerror: (error) => reject(error), ontimeout: () => reject(new Error("Request timeout")), }); }); } const response = await GM_xhr({ method: 'GET', url: url.includes('http') ? url : location.origin + url, headers: { 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1 Edg/137.0.0.0" || navigator.userAgent, "charset": "utf-8", "acceptEncoding": "gzip, deflate" }, overrideUserAgent: true, responseType: "arraybuffer", onload: function(response) { /* ... */ } }); let text = response.responseText; if(text.includes('meta charset=\"gbk\"')){ // 用 GBK 解码 const decoder = new TextDecoder("gbk"); const decodedText = decoder.decode(new Uint8Array(response.response)); text = decodedText; }else{ const decoder = new TextDecoder("utf-8"); const decodedText = decoder.decode(new Uint8Array(response.response)); text = decodedText; } if(text.includes('document.writeln')){ console.log('iframe获取') currentChapter = await extractContentByIframe(text) }else{ // 创建临时DOM解析 const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); currentChapter = extractContent(doc) } showChapter(); } catch (error) { console.error('加载失败:', error); try{ //针对顽固性网站 currentChapter = await extractContentByIframe2(url); showChapter(); }catch(error2){ const textEl = document.getElementById('simple-reader-text'); // 获取所有属性(包括不可枚举的) const errorDetails = {}; Object.getOwnPropertyNames(error).forEach(key => { errorDetails[key] = error[key]; }); textEl.innerHTML += `

章节加载失败

${JSON.stringify(errorDetails)}

`; } } finally { isLoading = false; loadingEl.style.display = 'none'; } } // 显示章节内容 function showChapter() { const textEl = document.getElementById('simple-reader-text'); cacheChapter.push(currentChapter); textEl.appendChild(currentChapter.content); if(cacheChapter.length > 5) { const oldChapter = cacheChapter.shift(); oldChapter.content.remove(); } // 自动检查是否需要加载下一章 checkAutoLoad(); } // 检查是否需要自动加载下一章 function checkAutoLoad() { if (isLoading || !currentChapter.nextUrl) return; const reader = document.getElementById('simple-reader'); const { scrollTop, scrollHeight, clientHeight } = reader; const distanceToBottom = scrollHeight - (scrollTop + clientHeight); // 距离底部小于300px时加载下一章 if (distanceToBottom < 500) { loadChapter(currentChapter.nextUrl); } } function initNovelReader(event){ window.stop(); document.open('text/html', 'replace'); document.write(cleanHTML); document.close(); document._createElement = document.createElement; document.createElement = function(tagName) { console.log('createElement:', tagName) }; // 保存原始方法 if (window.navigation) { window.navigation.addEventListener('navigate', (event) => { console.log('导航尝试:', event.destination.url); event.preventDefault(); // 阻止导航 if(getNexturlByClick){ currentChapter.nextUrl = event.destination.url; getNexturlByClick = false; } }); } loadChapter(window.location.href) const reader = document.getElementById('simple-reader'); reader.style.display = 'block'; reader.addEventListener('scroll', checkAutoLoad); try{ // 注销所有 Service Worker navigator.serviceWorker.getRegistrations().then(registrations => { console.log('清理:', registrations) registrations.forEach(registration => registration.unregister()); }); }catch(err){} } GM_registerMenuCommand('启动小说阅读模式', initNovelReader); // 页面加载完成后添加按钮 //if (document.readyState === 'complete') { // addLaunchButton(); //} else { // window.addEventListener('load', addLaunchButton); //} })();