// ==UserScript== // @name 阅读模式 (覆盖层·纯净版) // @namespace https://viayoo.com/hqzw7k // @version 18.3.2 // @description 在当前页面之上叠加一层纯净阅读界面,原页面被锁定在下方,退出后自动恢复。脚本自动识别正文、标题及章节翻页链接(上一页/下一页/目录),并有效过滤视频、样式、脚本、按钮、模拟按钮等干扰元素。默认保留所有图片,您可通过 EXTRA_REMOVE_SELECTORS 自定义需要整体移除的区域(如下载卡片)。支持背景色切换、字号调节、“新标签页”打开等实用功能。适配小说站、博客、新闻等各类网站,提供沉浸式阅读体验。 // @author Grok & DeepSeek // @license MIT // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @run-at document-end // ==/UserScript== (function() { 'use strict'; // ==================== 配置区 ==================== // 正文选择器(按优先级排序) const CONTENT_SELECTORS = [ // 小说/文学网站专用(中文站点高优先级) '#chapter-content', '#chaptercontent', '#ChapterContent', '#nr1', '#nr_title', '#booktext', '#novel_content', '#read-content', '#chapter-body', '#txtContent', '#BookText', '#chapter', '#contentBox', '#book_read', '.read-content', '.chapter-content', '#J_read', '#reader', '.article-content', '.content-area', '.post-body', '#js-read__content', '#readbox', '#zhengwen', '#kui-page-read-txt', '#chapter_cont', '#BookTextRead', '#book_text', '#readtext', '#readcon', '#TextContent', '#text_c', '#txt_td', '#TXT', '#txt', '#zjneirong', '.novel_content', '.readmain_inner', '.noveltext', '.booktext', '.yd_text2', '#contentTxt', '#oldtext', '#a_content', '#contents', '#content2', '#contentts', '#content1', '#novelcontent', '#text', '.card-list', '.m-post', '#content', // 微信公众号专用 '#js_content', // 博客/新闻/文章通用 '#article', '#articleBody', '#article-body', '#post-content', '#entry-content', '#main-content', '#content-main', '#post', '.article', '.article-body', '.post-content', '.entry-content', '.post-body', '.entry-body', '.markdown-body', '.article-content', '.content-inner', // 语义化标签 & 常见框架 'article', '[role="main"]', '[role="article"]', '[itemprop="articleBody"]', '[itemprop="text"]', '.post', '.blog-post', '.story', '.content-wrapper', '.readable-content', '.post-article', // 极简 fallback '.text', '.body', '.article-text', '.page-content', '#pagecontent', '#contentbox', '#bmsy_content', '#bookpartinfo', '#htmlContent', '#text_area' ]; // 标题选择器 const TITLE_SELECTORS = [ 'h1.article-title', 'h1.entry-title', 'h1.post-title', 'h1.title', 'h1#title', 'h1#articleTitle', 'h1[itemprop="headline"]', '.nr_function>h1', 'h1', '.article-title', '.entry-title', '.post-title', '.title', '#title', '#article-title', '#post-title', '.title-color' ]; // 导航链接选择器(上一页、下一页、目录) const NAV_SELECTORS = { prev: [ 'a:contains(上一页)', 'a:contains(上一章)', 'a:contains(上一篇)', 'a.prev', 'a.previous', '.prev', '.previous', '#prev', '#previous', 'a[rel="prev"]', 'a[rel="previous"]', 'a[data-prev]' ], next: [ 'a:contains(下一页)', 'a:contains(下一章)', 'a:contains(下一篇)', 'a.next', '.next', '#next', 'a[rel="next"]', 'a[data-next]' ], index: [ 'a:contains(目录)', 'a:contains(章节目录)', 'a:contains(返回目录)', 'a:contains(书籍页)', 'a:contains(章节列表)', 'a.index', '.index', '#index', 'a.toc', '.toc', '#toc', 'a[rel="index"]' ] }; // 额外需要整体移除的元素选择器(支持 CSS 选择器) const EXTRA_REMOVE_SELECTORS = [ '.aritcle_card', // 示例:移除包含图片和下载链接的卡片 ]; // 判断是否为新标签页模式(URL 带有 #readermode) const SHOULD_ACTIVATE_NEW_TAB = window.location.hash === '#readermode'; // ==================== 全局变量 ==================== let isReaderMode = false; let buttonHideTimeout = null; let mainButton = null; let originalUrl = ''; let menuCommandId = null; let readerOverlay = null; let newTabOverlay = null; let contentObserver = null; let originalContentElement = null; // 保存原页面滚动状态 let originalBodyOverflow = ''; let originalBodyPosition = ''; let originalBodyTop = ''; let originalBodyWidth = ''; let scrollY = 0; // 背景色与字号预设 const config = { backgroundOptions: { '默认白': '#ffffff', '豆沙绿': '#c7edcc', '护眼绿': '#e3f2e1', '护眼黑': '#1a1a1a' }, fontSizeOptions: { '小': '16px', '中': '18px', '大': '20px', '特大': '22px' } }; // ==================== 辅助函数 ==================== function isValidHref(href) { if (!href || href === '#' || href.trim() === '') return false; if (href.startsWith('javascript:')) { const voidPattern = /^javascript:void\s*\(?\s*0\s*\)?\s*;?$/i; return !voidPattern.test(href); } return true; } // 清理模拟按钮 function removeSimulatedButtons(clone) { clone.querySelectorAll('[role="button"]:not(a)').forEach(el => el.remove()); clone.querySelectorAll('[rl-type="stop"]:not(a)').forEach(el => el.remove()); const keywords = ['btn', 'button', 'copy', 'share', 'feedback', 'thumb', 'like', 'dislike', 'close', 'popup', 'modal', 'toolbar', 'interact', 'action']; const selector = keywords.map(kw => `[class*="${kw}"]:not(a)`).join(','); clone.querySelectorAll(selector).forEach(el => el.remove()); } // 清理正文克隆(默认保留所有图片) function cleanContent(clone) { // 移除额外选择器匹配的整个元素(如推广卡片) for (const selector of EXTRA_REMOVE_SELECTORS) { const elements = clone.querySelectorAll(selector); elements.forEach(el => el.remove()); } // 清理其他干扰元素 clone.querySelectorAll('video, style, link[rel="stylesheet"], script, button').forEach(el => el.remove()); clone.querySelectorAll('input[type="button"], input[type="submit"], input[type="reset"]').forEach(el => el.remove()); removeSimulatedButtons(clone); // 移除所有内联样式 clone.querySelectorAll('*').forEach(el => { if (el.hasAttribute('style')) el.removeAttribute('style'); }); } // 通过选择器中的 :contains 文本查找链接(模拟 jQuery) function findLinkByText(selectors) { for (const selector of selectors) { if (selector.includes(':contains(')) { const match = selector.match(/:contains\((.+?)\)/); if (match) { const text = match[1]; const baseSelector = selector.replace(/:contains\(.+?\)/, ''); const elements = baseSelector ? document.querySelectorAll(baseSelector) : document.querySelectorAll('a'); for (const el of elements) { if (el.tagName === 'A' && el.href && el.textContent.includes(text) && isValidHref(el.href)) { return el; } } } } else { const el = document.querySelector(selector); if (el && el.tagName === 'A' && el.href && isValidHref(el.href)) { return el; } } } return null; } // 通过上下文(XPath)查找导航链接 function findNavLinksByContext() { const result = { prev: null, next: null, index: null }; const xpaths = { prev: '//*[contains(text(),"上一章") or contains(text(),"上一页") or contains(text(),"上一篇")]/following-sibling::a[1]', next: '//*[contains(text(),"下一章") or contains(text(),"下一页") or contains(text(),"下一篇")]/following-sibling::a[1]', index: '//*[contains(text(),"目录") or contains(text(),"章节目录") or contains(text(),"返回目录") or contains(text(),"书籍页")]/following-sibling::a[1]' }; for (let key in xpaths) { const node = document.evaluate(xpaths[key], document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (node && node.tagName === 'A' && node.href && isValidHref(node.href)) { result[key] = node; } } // 备选:查找包含关键文本的容器内的第一个链接 const containers = [ { key: 'prev', texts: ['上一章', '上一页', '上一篇'] }, { key: 'next', texts: ['下一章', '下一页', '下一篇'] }, { key: 'index', texts: ['目录', '章节目录', '返回目录', '书籍页'] } ]; for (const item of containers) { if (!result[item.key]) { for (const txt of item.texts) { const container = document.evaluate(`//*[contains(text(),"${txt}")]`, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (container) { const link = container.querySelector('a[href]'); if (link && isValidHref(link.href)) { result[item.key] = link; break; } } } } } return result; } // 整合查找导航链接 function findNavLinks() { let links = { prev: findLinkByText(NAV_SELECTORS.prev), next: findLinkByText(NAV_SELECTORS.next), index: findLinkByText(NAV_SELECTORS.index) }; const contextLinks = findNavLinksByContext(); if (!links.prev && contextLinks.prev) links.prev = contextLinks.prev; if (!links.next && contextLinks.next) links.next = contextLinks.next; if (!links.index && contextLinks.index) links.index = contextLinks.index; return links; } // 提取正文、标题和导航链接 function extractContent() { let contentElement = null; for (const selector of CONTENT_SELECTORS) { const elements = document.querySelectorAll(selector); if (elements.length) { let maxElem = elements[0]; let maxLen = elements[0].innerHTML.length; for (let i = 1; i < elements.length; i++) { const len = elements[i].innerHTML.length; if (len > maxLen) { maxLen = len; maxElem = elements[i]; } } contentElement = maxElem; // console.log(`阅读模式: 使用选择器 "${selector}"`); break; } } if (!contentElement) { // console.log('阅读模式: 未找到正文元素'); return null; } originalContentElement = contentElement; const clone = contentElement.cloneNode(true); cleanContent(clone); if (!clone.textContent.trim()) { // console.log('阅读模式: 正文内容为空'); return null; } let title = ''; for (const selector of TITLE_SELECTORS) { const el = document.querySelector(selector); if (el && el.textContent.trim()) { title = el.textContent.trim(); // console.log(`阅读模式: 使用标题选择器 "${selector}" 匹配到标题: "${title}"`); break; } } if (!title) { title = document.title || ''; // console.log(`阅读模式: 使用网页标题: "${title}"`); } const navLinks = findNavLinks(); return { title: title, content: clone.innerHTML, nav: navLinks }; } // 为 URL 添加 #readermode 哈希 function addReaderModeHash(url) { try { const u = new URL(url, window.location.origin); u.hash = 'readermode'; return u.href; } catch (e) { return url + '#readermode'; } } // 根据背景色计算衍生颜色 function calculateColors(backgroundColor) { const isDarkMode = backgroundColor === '#1a1a1a'; return { textColor: isDarkMode ? '#e0e0e0' : '#000', borderColor: isDarkMode ? '#444' : '#eee', controlBgColor: isDarkMode ? '#333' : 'white', controlBorderColor: isDarkMode ? '#555' : '#ddd', subtleTextColor: isDarkMode ? '#aaa' : '#666', exitBtnColor: isDarkMode ? '#999' : '#666', navBtnColor: isDarkMode ? '#4a9eff' : '#007bff' }; } // 对正文容器统一应用样式 function processContent(container, backgroundColor) { const colors = calculateColors(backgroundColor); container.querySelectorAll('p').forEach((p, idx) => { p.style.cssText = ` text-indent: 0; line-height: 1.8; color: ${colors.textColor}; margin: ${idx === 0 ? '0' : '1.8em'} 0 1.8em 0; text-align: justify; padding: 0; user-select: text; `; }); container.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(h => { h.style.cssText = ` color: ${colors.textColor}; font-weight: 600; line-height: 1.4; text-align: left; font-size: 1.2em; margin: 1.2em 0 0.6em 0; user-select: text; `; }); container.querySelectorAll('div, span, li, td').forEach(el => { if (!el.matches('h1, h2, h3, h4, h5, h6')) { el.style.color = colors.textColor; el.style.userSelect = 'text'; } }); } // 创建导航栏(上一页/目录/下一页),如果没有有效链接则返回 null function createNavBar(navLinks, colors, addHash = false) { const hasAnyLink = navLinks.prev || navLinks.next || navLinks.index; if (!hasAnyLink) return null; const bar = document.createElement('div'); bar.className = 'reader-nav-bar'; bar.style.cssText = 'display: flex; justify-content: center; gap: 20px; margin: 20px 0; font-size: 15px;'; const createButton = (text, link) => { const a = document.createElement('a'); a.textContent = text; let finalHref = link ? link.href : '#'; if (link && addHash) { if (link.href.startsWith('javascript:')) { finalHref = link.href; } else { finalHref = addReaderModeHash(link.href); } } a.href = finalHref; a.style.cssText = ` color: ${link ? colors.navBtnColor : '#999'}; text-decoration: none; cursor: ${link ? 'pointer' : 'default'}; opacity: ${link ? 1 : 0.5}; background: none; font-weight: normal; `; if (!link) a.addEventListener('click', e => e.preventDefault()); return a; }; bar.appendChild(createButton('上一页', navLinks.prev)); bar.appendChild(createButton('目录', navLinks.index)); bar.appendChild(createButton('下一页', navLinks.next)); return bar; } // 创建顶部控制栏(移除了图片开关) function createControlBar(backgroundColor, fontSize, onBgChange, onFontSizeChange, onExit, originalUrl) { const colors = calculateColors(backgroundColor); const bar = document.createElement('div'); bar.id = 'reader-control-bar'; bar.style.cssText = `margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid ${colors.borderColor};`; const row = document.createElement('div'); row.style.cssText = `display: flex; justify-content: space-between; align-items: center; font-size: 14px; color: ${colors.subtleTextColor};`; // 左侧:新标签页按钮 const leftGroup = document.createElement('div'); const newTabBtn = document.createElement('a'); newTabBtn.textContent = '新标签页'; newTabBtn.href = addReaderModeHash(originalUrl || window.location.href); newTabBtn.target = '_blank'; newTabBtn.style.cssText = ` color: ${colors.navBtnColor}; text-decoration: none; padding: 4px 12px; border: 1px solid ${colors.borderColor}; border-radius: 16px; font-size: 13px; transition: background 0.2s; `; newTabBtn.addEventListener('mouseenter', () => newTabBtn.style.background = colors.controlBgColor); newTabBtn.addEventListener('mouseleave', () => newTabBtn.style.background = 'transparent'); leftGroup.appendChild(newTabBtn); // 中间:背景色和字号选择器 const settingsGroup = document.createElement('div'); settingsGroup.style.cssText = 'display: flex; gap: 15px;'; const bgSelector = document.createElement('select'); bgSelector.style.cssText = ` padding: 4px 8px; border: 1px solid ${colors.controlBorderColor}; border-radius: 3px; background: ${colors.controlBgColor}; color: ${colors.textColor}; cursor: pointer; font-size: 13px; `; Object.keys(config.backgroundOptions).forEach(name => { const option = document.createElement('option'); option.value = config.backgroundOptions[name]; option.textContent = name; if (config.backgroundOptions[name] === backgroundColor) option.selected = true; bgSelector.appendChild(option); }); const fontSizeSelector = document.createElement('select'); fontSizeSelector.style.cssText = bgSelector.style.cssText; Object.keys(config.fontSizeOptions).forEach(name => { const option = document.createElement('option'); option.value = config.fontSizeOptions[name]; option.textContent = name; if (config.fontSizeOptions[name] === fontSize) option.selected = true; fontSizeSelector.appendChild(option); }); settingsGroup.appendChild(bgSelector); settingsGroup.appendChild(fontSizeSelector); // 右侧:退出按钮 const exitBtn = document.createElement('span'); exitBtn.textContent = '退出'; exitBtn.style.cssText = `color: ${colors.exitBtnColor}; cursor: pointer; text-decoration: underline;`; exitBtn.addEventListener('click', onExit); row.appendChild(leftGroup); row.appendChild(settingsGroup); row.appendChild(exitBtn); bar.appendChild(row); bgSelector.addEventListener('change', e => onBgChange(e.target.value)); fontSizeSelector.addEventListener('change', e => onFontSizeChange(e.target.value)); return bar; } // 更新阅读器内容(用于动态网站翻页时刷新) function updateReaderContent(overlay, article) { if (!overlay || !article) return; const titleEl = overlay.querySelector('#reader-article-title'); const contentSec = overlay.querySelector('#reader-content-section'); const topNav = overlay.querySelector('#reader-top-nav'); const bottomNav = overlay.querySelector('#reader-bottom-nav'); if (titleEl) titleEl.textContent = article.title || ''; if (contentSec) { contentSec.innerHTML = article.content; const bgColor = overlay.style.backgroundColor || '#ffffff'; processContent(contentSec, bgColor); } const colors = calculateColors(overlay.style.backgroundColor || '#ffffff'); const newTop = createNavBar(article.nav, colors, true); const newBottom = createNavBar(article.nav, colors, true); if (topNav && newTop) topNav.replaceWith(newTop); else if (topNav && !newTop) topNav.remove(); if (bottomNav && newBottom) bottomNav.replaceWith(newBottom); else if (bottomNav && !newBottom) bottomNav.remove(); } // ==================== 构建阅读界面 ==================== // 覆盖模式:在当前页面添加遮罩层 function buildOverlayReaderUI(article, initialBgColor = '#ffffff', initialFontSize = '18px') { const overlay = document.createElement('div'); overlay.id = 'reader-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: ${initialBgColor}; z-index: 2147483647; isolation: isolate; transform: translateZ(0); will-change: transform; overflow-y: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; `; const mainContainer = document.createElement('div'); mainContainer.id = 'reader-container'; mainContainer.style.cssText = ` width: 92%; margin: 0 auto; padding: 20px 10px; color: ${calculateColors(initialBgColor).textColor}; font-size: ${initialFontSize}; line-height: 1.8; user-select: text; `; const contentArea = document.createElement('div'); contentArea.id = 'reader-content-area'; contentArea.style.cssText = 'text-align: justify;'; const controlBar = createControlBar( initialBgColor, initialFontSize, (newColor) => { overlay.style.background = newColor; const colors = calculateColors(newColor); mainContainer.style.color = colors.textColor; processContent(contentSection, newColor); if (articleTitle) articleTitle.style.color = colors.textColor; }, (newSize) => { mainContainer.style.fontSize = newSize; contentSection.style.fontSize = newSize; }, exitLegacyReaderMode, originalUrl ); let articleTitle = null; if (article.title && article.title.trim()) { articleTitle = document.createElement('h1'); articleTitle.id = 'reader-article-title'; articleTitle.textContent = article.title; articleTitle.style.cssText = ` margin: 0 0 30px 0; font-size: 1.4em; font-weight: 700; line-height: 1.4; color: ${calculateColors(initialBgColor).textColor}; text-align: left; user-select: text; `; } const contentSection = document.createElement('div'); contentSection.id = 'reader-content-section'; contentSection.innerHTML = article.content; contentSection.style.fontSize = initialFontSize; processContent(contentSection, initialBgColor); const topNavBar = createNavBar(article.nav, calculateColors(initialBgColor), false); const bottomNavBar = createNavBar(article.nav, calculateColors(initialBgColor), false); contentArea.appendChild(controlBar); if (articleTitle) contentArea.appendChild(articleTitle); if (topNavBar) contentArea.appendChild(topNavBar); contentArea.appendChild(contentSection); if (bottomNavBar) contentArea.appendChild(bottomNavBar); mainContainer.appendChild(contentArea); overlay.appendChild(mainContainer); document.documentElement.appendChild(overlay); overlay.focus(); setTimeout(() => { const ctrlBar = overlay.querySelector('#reader-control-bar'); if (ctrlBar) { const rect = ctrlBar.getBoundingClientRect(); overlay.scrollTo(0, rect.top + overlay.scrollTop + rect.height); } }, 0); return overlay; } // 新标签页模式:隐藏原页面内容,创建遮罩层,并监听内容变化 function buildNewTabReaderUI(article, initialBgColor = '#ffffff', initialFontSize = '18px') { const hiddenContainer = document.createElement('div'); hiddenContainer.id = 'original-page-hidden'; hiddenContainer.style.cssText = 'display: none;'; while (document.body.firstChild) { hiddenContainer.appendChild(document.body.firstChild); } document.body.appendChild(hiddenContainer); const overlay = document.createElement('div'); overlay.id = 'reader-newtab-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: ${initialBgColor}; z-index: 2147483647; isolation: isolate; transform: translateZ(0); will-change: transform; overflow-y: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; `; const mainContainer = document.createElement('div'); mainContainer.id = 'reader-container'; mainContainer.style.cssText = ` width: 92%; margin: 0 auto; padding: 20px 10px; color: ${calculateColors(initialBgColor).textColor}; font-size: ${initialFontSize}; line-height: 1.8; user-select: text; `; const contentArea = document.createElement('div'); contentArea.id = 'reader-content-area'; contentArea.style.cssText = 'text-align: justify;'; const controlBar = createControlBar( initialBgColor, initialFontSize, (newColor) => { overlay.style.background = newColor; const colors = calculateColors(newColor); mainContainer.style.color = colors.textColor; if (contentSection) processContent(contentSection, newColor); if (articleTitle) articleTitle.style.color = colors.textColor; }, (newSize) => { mainContainer.style.fontSize = newSize; if (contentSection) contentSection.style.fontSize = newSize; }, () => window.close(), originalUrl ); let articleTitle = null; if (article.title && article.title.trim()) { articleTitle = document.createElement('h1'); articleTitle.id = 'reader-article-title'; articleTitle.textContent = article.title; articleTitle.style.cssText = ` margin: 0 0 30px 0; font-size: 1.4em; font-weight: 700; line-height: 1.4; color: ${calculateColors(initialBgColor).textColor}; text-align: left; user-select: text; `; } const contentSection = document.createElement('div'); contentSection.id = 'reader-content-section'; contentSection.innerHTML = article.content; contentSection.style.fontSize = initialFontSize; processContent(contentSection, initialBgColor); const topNavBar = createNavBar(article.nav, calculateColors(initialBgColor), true); const bottomNavBar = createNavBar(article.nav, calculateColors(initialBgColor), true); contentArea.appendChild(controlBar); if (articleTitle) contentArea.appendChild(articleTitle); if (topNavBar) contentArea.appendChild(topNavBar); contentArea.appendChild(contentSection); if (bottomNavBar) contentArea.appendChild(bottomNavBar); mainContainer.appendChild(contentArea); overlay.appendChild(mainContainer); document.documentElement.appendChild(overlay); overlay.focus(); if (originalContentElement) { contentObserver = new MutationObserver(() => { const newArticle = extractContent(); if (newArticle) { updateReaderContent(overlay, newArticle); } }); contentObserver.observe(originalContentElement, { childList: true, subtree: true, characterData: true }); } setTimeout(() => { const ctrlBar = overlay.querySelector('#reader-control-bar'); if (ctrlBar) { const rect = ctrlBar.getBoundingClientRect(); overlay.scrollTo(0, rect.top + overlay.scrollTop + rect.height); } }, 0); newTabOverlay = overlay; return overlay; } // ==================== 浮动按钮 ==================== function clearButtonTimer() { if (buttonHideTimeout) { clearTimeout(buttonHideTimeout); buttonHideTimeout = null; } } function createMainButtonOnce() { if (mainButton) return; const btn = document.createElement('button'); btn.id = 'reader-main-btn'; btn.textContent = '阅读模式'; btn.style.cssText = ` position: fixed; bottom: 60px; right: 20px; z-index: 10000; padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; opacity: 0.9; box-shadow: 0 2px 8px rgba(0,0,0,0.2); transition: opacity 0.3s; user-select: none; `; btn.addEventListener('mouseenter', () => { if (isReaderMode) return; btn.style.opacity = '1'; clearButtonTimer(); }); btn.addEventListener('mouseleave', () => { if (isReaderMode) return; btn.style.opacity = '0.9'; clearButtonTimer(); buttonHideTimeout = setTimeout(() => hideButton(btn), 3000); }); btn.addEventListener('click', enterLegacyReaderMode); document.body.appendChild(btn); mainButton = btn; buttonHideTimeout = setTimeout(() => hideButton(btn), 3000); } function hideButton(btn) { if (!isReaderMode && btn && btn.parentNode) { btn.style.opacity = '0'; setTimeout(() => { if (btn.parentNode && !isReaderMode) { btn.style.display = 'none'; } }, 300); } } function hideMainButtonPermanently() { if (mainButton) { clearButtonTimer(); mainButton.style.display = 'none'; } } // ==================== 模式切换核心逻辑 ==================== function enterLegacyReaderMode() { if (isReaderMode) return; hideMainButtonPermanently(); setTimeout(() => { const article = extractContent(); if (!article) { alert('未找到可阅读的正文内容'); return; } originalUrl = window.location.href.replace(/#.*$/, ''); scrollY = window.scrollY; const body = document.body; originalBodyOverflow = body.style.overflow; originalBodyPosition = body.style.position; originalBodyTop = body.style.top; originalBodyWidth = body.style.width; body.style.overflow = 'hidden'; body.style.position = 'fixed'; body.style.top = `-${scrollY}px`; body.style.width = '100%'; readerOverlay = buildOverlayReaderUI(article); isReaderMode = true; updateMenuCommands(); }, 50); } function exitLegacyReaderMode() { if (!isReaderMode || !readerOverlay) return; readerOverlay.remove(); readerOverlay = null; const body = document.body; body.style.overflow = originalBodyOverflow; body.style.position = originalBodyPosition; body.style.top = originalBodyTop; body.style.width = originalBodyWidth; window.scrollTo(0, scrollY); isReaderMode = false; updateMenuCommands(); } function activateNewTabReaderMode() { let attempts = 0; const maxAttempts = 10; const tryExtract = () => { const article = extractContent(); if (article) { originalUrl = window.location.href.replace(/#.*$/, ''); buildNewTabReaderUI(article); updateMenuCommands(); } else { attempts++; if (attempts < maxAttempts) { // console.log(`阅读模式: 等待内容加载 (${attempts}/${maxAttempts})`); setTimeout(tryExtract, 300); } else { alert('未找到可阅读的正文内容,可能加载超时'); window.close(); } } }; tryExtract(); } function openReaderInNewTab() { const url = addReaderModeHash(window.location.href); window.open(url, '_blank'); } // ==================== 油猴菜单管理 ==================== function updateMenuCommands() { if (menuCommandId !== null) { GM_unregisterMenuCommand(menuCommandId); menuCommandId = null; } if (SHOULD_ACTIVATE_NEW_TAB) { menuCommandId = GM_registerMenuCommand('❌ 退出阅读模式', () => window.close()); } else if (isReaderMode) { menuCommandId = GM_registerMenuCommand('❌ 退出阅读模式', exitLegacyReaderMode); } else { menuCommandId = GM_registerMenuCommand('打开阅读模式', enterLegacyReaderMode); GM_registerMenuCommand('新标签页打开阅读模式', openReaderInNewTab); } } // ==================== 初始化(增加重试机制) ==================== function init() { if (SHOULD_ACTIVATE_NEW_TAB) { activateNewTabReaderMode(); } else { let retryCount = 0; const maxRetries = 10; // 最多重试10次 const retryInterval = 200; // 每200ms检查一次 const maxWaitTime = 5000; // 最大等待5秒 const startTime = Date.now(); const intervalId = setInterval(() => { const testArticle = extractContent(); // 如果成功提取到正文,或者等待超时 if (testArticle || (Date.now() - startTime) >= maxWaitTime) { clearInterval(intervalId); if (testArticle) { createMainButtonOnce(); } updateMenuCommands(); } else { retryCount++; // 可选:在控制台输出重试信息(默认注释) // console.log(`阅读模式: 等待正文加载 (${retryCount}/${maxRetries})`); } }, retryInterval); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();