// ==UserScript== // @name 阅读模式 (覆盖层·纯净版) // @namespace https://viayoo.com/hqzw7k // @version 19.5.0 // @description 在当前页面之上叠加一层纯净阅读界面,原页面锁定于下方,退出后自动恢复。自动识别正文、标题及翻页链接,过滤视频、样式、脚本、按钮等干扰,支持背景色切换、字号调节、新标签页打开。 // @author DeepSeek & Grok // @license MIT // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_setValue // @grant GM_getValue // @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', '.m-post','.novel-content','pre.novel-content-txt','.share-v2-chapter-content', '.content_ul','#reader-container','.file_content','.view_one .comiis_a', '#article','#articleBody','#article-body','#post-content','#entry-content', '#main-content','#content-main','#post','.article','.article-body', '.post-content','.entry-content','.post-body','.entry-body', '.article-content','.content-inner','.articleText','#js_content', '#baikan-content','.news-content','#content', 'article','[role="main"]','[role="article"]', '[itemprop="articleBody"]','[itemprop="text"]','.post','.blog-post', '.story','.content-wrapper','.readable-content','.post-article' ]; 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','h2.big.o' ]; 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"]' ] }; const EXTRA_REMOVE_SELECTORS = [ '.aritcle_card','.projects-header','.card-list','.apd-bg','.apd', '#comments','.comments','.url-card' ]; const SHOULD_ACTIVATE_NEW_TAB = window.location.hash === '#readermode'; // ==================== 油猴存储配置 ==================== const STORAGE_KEYS = { BG_COLOR: 'reader_bg_color', FONT_SIZE: 'reader_font_size' }; const DEFAULT_SETTINGS = { bgColor: '#ffffff', fontSize: '18px' }; const settingsCache = {}; function gmGet(key, def) { if (settingsCache[key] !== undefined) return settingsCache[key]; try { const v = GM_getValue(key); if (v !== undefined && v !== null && v !== '') { settingsCache[key] = v; return v; } } catch(e) {} return def; } function gmSet(key, val) { settingsCache[key] = val; try { GM_setValue(key, val); } catch(e) {} } function loadSettings() { return { bgColor: gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor), fontSize: gmGet(STORAGE_KEYS.FONT_SIZE, DEFAULT_SETTINGS.fontSize) }; } function saveSetting(key, value) { gmSet(key, value); } // ==================== 全局变量 ==================== let isReaderMode = false; let buttonHideTimeout = null; let mainButton = null; let originalUrl = ''; let menuCommandIds = []; 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', '藕粉': '#f5e6e8', '护眼黑': '#1a1a1a' }, fontSizeOptions: { '小': '16px', '中': '18px', '大': '20px', '特大': '22px' } }; // ==================== 辅助函数 ==================== function isValidHref(h) { if (!h || h.trim() === '' || h === '#') return false; try { const url = new URL(h, window.location.href); return url.protocol === 'http:' || url.protocol === 'https:'; } catch (e) { return false; } } function removeSimulatedButtons(c) { c.querySelectorAll('[role="button"]:not(a)').forEach(e => e.remove()); c.querySelectorAll('[rl-type="stop"]:not(a)').forEach(e => e.remove()); const kw = ['btn','button','copy','share','feedback','thumb','like','dislike','close','popup','modal','toolbar','interact','action']; const s = kw.map(k => `[class*="${k}"]:not(a)`).join(','); c.querySelectorAll(s).forEach(e => e.remove()); } function stripLeadingSpaces(c) { const blockTags = ['p','div','li','blockquote','h1','h2','h3','h4','h5','h6']; c.querySelectorAll(blockTags.join(',')).forEach(el => { const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null); const first = walker.nextNode(); if (first) { first.nodeValue = first.nodeValue.replace(/^[\s\u00A0\u3000\u2000-\u200B\uFEFF]+/, ''); } }); c.querySelectorAll('pre').forEach(pre => { const first = pre.firstChild; if (first && first.nodeType === 3 && /^\s*$/.test(first.nodeValue)) first.nodeValue = ''; }); } function cleanContent(c) { // 1. 移除明确隐藏的元素(display:none / visibility:hidden) c.querySelectorAll('[style*="display: none"], [style*="display:none"], [style*="visibility: hidden"], [style*="visibility:hidden"]').forEach(e => e.remove()); // 2. 解包 markdown-accessiblity-table 自定义元素 c.querySelectorAll('markdown-accessiblity-table').forEach(el => { const p = el.parentNode; if (p) { while (el.firstChild) p.insertBefore(el.firstChild, el); p.removeChild(el); } }); // 3. 移除额外选择器匹配的整块元素(含广告位、评论区、相关推荐) for (const s of EXTRA_REMOVE_SELECTORS) c.querySelectorAll(s).forEach(e => e.remove()); // 4. 移除视频、样式、脚本、按钮、SVG、表单控件 c.querySelectorAll('video, style, link[rel="stylesheet"], script, button, svg').forEach(e => e.remove()); c.querySelectorAll('form, textarea, select, input').forEach(e => e.remove()); removeSimulatedButtons(c); // 5. 移除空图标标签 c.querySelectorAll('i').forEach(e => { if (!e.textContent.trim() && !e.querySelector('img')) e.remove(); }); // 6. 移除所有内联样式与 class c.querySelectorAll('*').forEach(e => { e.removeAttribute('style'); e.removeAttribute('class'); }); // 7. 解包 span / mark / ml-search ['mark','span','ml-search'].forEach(t => { c.querySelectorAll(t).forEach(e => { const p = e.parentNode; if (p) { while (e.firstChild) p.insertBefore(e.firstChild, e); p.removeChild(e); } }); }); // 8. 链接只保留 href c.querySelectorAll('a').forEach(a => { const h = a.getAttribute('href'); if (!h || !isValidHref(h)) { const p = a.parentNode; if (p) { while (a.firstChild) p.insertBefore(a.firstChild, a); p.removeChild(a); } return; } const attrs = a.attributes; for (let i = attrs.length - 1; i >= 0; i--) { if (attrs[i].name !== 'href') a.removeAttribute(attrs[i].name); } }); // 9. 图片只保留 src / alt c.querySelectorAll('img').forEach(img => { const src = img.getAttribute('src'); const attrs = img.attributes; for (let i = attrs.length - 1; i >= 0; i--) { const attr = attrs[i]; if (attr.name !== 'src' && attr.name !== 'alt') img.removeAttribute(attr.name); } if (!src) img.remove(); }); // 10. 前导空白清理(含 pre) stripLeadingSpaces(c); } function detectIndent(el) { const ps = el.querySelectorAll('p'); if (ps.length === 0) return false; let cnt = 0; const n = Math.min(ps.length, 10); for (let i = 0; i < n; i++) { const p = ps[i]; const ti = parseFloat(window.getComputedStyle(p).textIndent) || 0; if (ti > 1) { cnt++; continue; } const walker = document.createTreeWalker(p, NodeFilter.SHOW_TEXT, null); const first = walker.nextNode(); // 仅将全角空格、  视为缩进空白;换行与普通空格不计入。 if (first && /^[\u3000\u00A0]/.test(first.nodeValue)) cnt++; } return cnt > n / 2; } function findLinkByText(selectors) { for (const s of selectors) { if (s.includes(':contains(')) { const m = s.match(/:contains\((.+?)\)/); if (m) { const text = m[1]; const base = s.replace(/:contains\(.+?\)/, ''); const els = base ? document.querySelectorAll(base) : document.querySelectorAll('a'); for (const el of els) { if (el.tagName === 'A' && el.href && el.textContent.includes(text) && isValidHref(el.href)) return el; } } } else { const el = document.querySelector(s); if (el && el.tagName === 'A' && el.href && isValidHref(el.href)) return el; } } return null; } function findNavLinksByContext() { const res = { 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)) res[key] = node; } const containers = [ { key: 'prev', texts: ['上一章','上一页','上一篇'] }, { key: 'next', texts: ['下一章','下一页','下一篇'] }, { key: 'index', texts: ['目录','章节目录','返回目录','书籍页'] } ]; for (const item of containers) { if (!res[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)) { res[item.key] = link; break; } } } } } return res; } function findNavLinks() { let links = { prev: findLinkByText(NAV_SELECTORS.prev), next: findLinkByText(NAV_SELECTORS.next), index: findLinkByText(NAV_SELECTORS.index) }; const cl = findNavLinksByContext(); if (!links.prev && cl.prev) links.prev = cl.prev; if (!links.next && cl.next) links.next = cl.next; if (!links.index && cl.index) links.index = cl.index; return links; } function extractContent() { let contentElement = null; for (const s of CONTENT_SELECTORS) { const els = document.querySelectorAll(s); if (els.length) { let max = els[0]; let maxLen = els[0].innerHTML.length; for (let i = 1; i < els.length; i++) { const len = els[i].innerHTML.length; if (len > maxLen) { maxLen = len; max = els[i]; } } contentElement = max; // console.log(`阅读模式: 使用选择器 "${s}"`); break; } } if (!contentElement) return null; originalContentElement = contentElement; const hasIndent = detectIndent(contentElement); const clone = contentElement.cloneNode(true); cleanContent(clone); const html = clone.innerHTML.trim(); const textContent = clone.textContent.trim(); if (!html || !textContent || textContent.length <= 20) return null; let title = ''; for (const s of TITLE_SELECTORS) { const el = document.querySelector(s); if (el && el.textContent.trim()) { title = el.textContent.trim(); // console.log(`阅读模式: 使用标题选择器 "${s}" 匹配到标题: "${title}"`); break; } } if (!title) { title = document.title || ''; // console.log(`阅读模式: 使用网页标题: "${title}"`); } return { title, content: html, nav: findNavLinks(), hasIndent }; } function addReaderModeHash(url) { try { const u = new URL(url, window.location.origin); u.hash = 'readermode'; u.searchParams.set('reader_trigger', 'menu'); return u.href; } catch(e) { let result = url; if (!result.includes('#')) result += '#readermode'; result += result.includes('?') ? '&reader_trigger=menu' : '?reader_trigger=menu'; return result; } } // ==================== 配色计算 ==================== // 代码块、行内代码、表格使用半透明叠加配色,能自适应各种背景色(含深色)。 function calculateColors(bg) { const dark = bg === '#1a1a1a'; return { textColor: dark ? '#e0e0e0' : '#000', borderColor: dark ? '#444' : '#eee', controlBgColor: dark ? '#333' : 'white', controlBorderColor: dark ? '#555' : '#ddd', subtleTextColor: dark ? '#aaa' : '#666', exitBtnColor: dark ? '#999' : '#666', navBtnColor: dark ? '#4a9eff' : '#007bff', codeBg: dark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)', codeBorder: dark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)', codeTextColor: dark ? '#e6edf3' : '#24292e', inlineCodeBg: dark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.06)', tableHeaderBg: dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)', tableBorder: dark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)' }; } function buildContentStyleCSS(bgColor, hasIndent) { const colors = calculateColors(bgColor); const indentRule = hasIndent ? '#reader-content-section p{text-indent:2em;}' : '#reader-content-section p{text-indent:0;}'; return ` #reader-content-section p, #reader-content-section div, #reader-content-section h1, #reader-content-section h2, #reader-content-section h3, #reader-content-section h4, #reader-content-section h5, #reader-content-section h6, #reader-content-section li, #reader-content-section blockquote, #reader-content-section pre, #reader-content-section table, #reader-content-section td, #reader-content-section th { max-width: 100%; overflow-wrap: break-word; word-wrap: break-word; box-sizing: border-box; } #reader-content-section p { text-align: justify; margin: 0 0 1.8em 0; } ${indentRule} #reader-content-section img { max-width: 100%; height: auto; } #reader-content-section blockquote { margin-left: 0; padding-left: 1.2em; border-left: 3px solid ${colors.borderColor}; color: ${colors.subtleTextColor}; } #reader-content-section a { color: ${colors.navBtnColor}; text-decoration: none; cursor: pointer; } #reader-content-section a:hover { text-decoration: none; } #reader-content-section strong, #reader-content-section b { font-weight: bold; } #reader-content-section em, #reader-content-section i { font-style: italic; } #reader-content-section h1, #reader-content-section h2, #reader-content-section h3, #reader-content-section h4, #reader-content-section h5, #reader-content-section h6 { font-weight: bold; margin-top: 1.2em; margin-bottom: 0.6em; } #reader-content-section h1 { font-size: 1.8em; } #reader-content-section h2 { font-size: 1.5em; } #reader-content-section h3 { font-size: 1.3em; } #reader-content-section ul, #reader-content-section ol { padding-left: 1.5em; margin: 0.5em 0; } #reader-content-section li { margin: 0.2em 0; } #reader-content-section code { background: ${colors.inlineCodeBg}; border-radius: 3px; padding: 0.15em 0.4em; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.92em; color: ${colors.codeTextColor}; } #reader-content-section pre { background: ${colors.codeBg}; border: 1px solid ${colors.codeBorder}; border-radius: 6px; padding: 12px 16px; overflow-x: auto; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; font-size: 0.92em; line-height: 1.6; white-space: pre; margin: 0 0 1.8em 0; } #reader-content-section pre code { background: none; border: none; padding: 0; border-radius: 0; font-size: inherit; color: inherit; } #reader-content-section table { border-collapse: collapse; width: 100%; margin: 1em 0; } #reader-content-section th, #reader-content-section td { border: 1px solid ${colors.tableBorder}; padding: 6px 10px; text-align: left; } #reader-content-section th { background: ${colors.tableHeaderBg}; font-weight: 600; } `; } function createNavBar(nav, colors, addHash = false, id = '') { if (!nav.prev && !nav.next && !nav.index) return null; const bar = document.createElement('div'); if (id) bar.id = id; bar.className = 'reader-nav-bar'; bar.style.cssText = 'display:flex;justify-content:center;gap:20px;margin:20px 0;font-size:15px;'; const createBtn = (text, link) => { const a = document.createElement('a'); a.textContent = text; let finalHref = link ? link.href : '#'; if (link && addHash) { finalHref = link.href.startsWith('javascript:') ? link.href : 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(createBtn('上一页', nav.prev)); bar.appendChild(createBtn('目录', nav.index)); bar.appendChild(createBtn('下一页', nav.next)); return bar; } function createControlBar(bg, fontSize, onBgChange, onFontSizeChange, onExit, origUrl) { const colors = calculateColors(bg); 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(origUrl || 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 opt = document.createElement('option'); opt.value = config.backgroundOptions[name]; opt.textContent = name; if (config.backgroundOptions[name] === bg) opt.selected = true; bgSelector.appendChild(opt); }); const fontSizeSelector = document.createElement('select'); fontSizeSelector.style.cssText = bgSelector.style.cssText; Object.keys(config.fontSizeOptions).forEach(name => { const opt = document.createElement('option'); opt.value = config.fontSizeOptions[name]; opt.textContent = name; if (config.fontSizeOptions[name] === fontSize) opt.selected = true; fontSizeSelector.appendChild(opt); }); 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'); const styleEl = overlay.querySelector('#reader-content-style'); if (titleEl) titleEl.textContent = article.title || ''; if (contentSec) { contentSec.innerHTML = article.content; const bg = overlay.style.backgroundColor || '#ffffff'; contentSec.style.color = calculateColors(bg).textColor; if (article.hasIndent !== undefined) contentSec.dataset.hasIndent = article.hasIndent ? 'true' : 'false'; } if (styleEl && article.hasIndent !== undefined) { styleEl.textContent = buildContentStyleCSS(overlay.style.backgroundColor || '#ffffff', article.hasIndent); } const colors = calculateColors(overlay.style.backgroundColor || '#ffffff'); const newTop = createNavBar(article.nav, colors, true, 'reader-top-nav'); const newBottom = createNavBar(article.nav, colors, true, 'reader-bottom-nav'); 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 buildReaderCore(article, initialBgColor, initialFontSize, onExit, overlayId, addHashToNav) { const overlay = document.createElement('div'); overlay.id = overlayId; overlay.style.cssText = `position:fixed;top:0;left:0;width:100%;height:100%;background:${initialBgColor};z-index:9999;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%;max-width:800px;margin:0 auto;padding:20px 10px;color:${calculateColors(initialBgColor).textColor};font-size:${initialFontSize};line-height:1.8;user-select:text;overflow-wrap:break-word;word-wrap:break-word;text-align:left;`; const contentArea = document.createElement('div'); contentArea.id = 'reader-content-area'; contentArea.style.cssText = 'text-align:left;'; let articleTitle = null; let contentSection = null; const style = document.createElement('style'); style.id = 'reader-content-style'; style.textContent = buildContentStyleCSS(initialBgColor, article.hasIndent); mainContainer.appendChild(style); const controlBar = createControlBar( initialBgColor, initialFontSize, (newColor) => { overlay.style.background = newColor; const colors = calculateColors(newColor); mainContainer.style.color = colors.textColor; if (contentSection) contentSection.style.color = colors.textColor; if (articleTitle) articleTitle.style.color = colors.textColor; const hasIndent = contentSection && contentSection.dataset.hasIndent === 'true'; style.textContent = buildContentStyleCSS(newColor, hasIndent); saveSetting(STORAGE_KEYS.BG_COLOR, newColor); }, (newSize) => { mainContainer.style.fontSize = newSize; if (contentSection) contentSection.style.fontSize = newSize; saveSetting(STORAGE_KEYS.FONT_SIZE, newSize); }, onExit, originalUrl ); 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;overflow-wrap:break-word;word-wrap:break-word;`; } contentSection = document.createElement('div'); contentSection.id = 'reader-content-section'; contentSection.innerHTML = article.content; contentSection.style.cssText = `font-size:${initialFontSize};line-height:1.8;color:${calculateColors(initialBgColor).textColor};user-select:text;overflow-wrap:break-word;word-wrap:break-word;max-width:100%;text-align:left;`; if (article.hasIndent !== undefined) contentSection.dataset.hasIndent = article.hasIndent ? 'true' : 'false'; const topNavBar = createNavBar(article.nav, calculateColors(initialBgColor), addHashToNav, 'reader-top-nav'); const bottomNavBar = createNavBar(article.nav, calculateColors(initialBgColor), addHashToNav, 'reader-bottom-nav'); 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 buildOverlayReaderUI(article, initialBgColor = '#ffffff', initialFontSize = '18px') { return buildReaderCore(article, initialBgColor, initialFontSize, exitLegacyReaderMode, 'reader-overlay', false); } 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 = buildReaderCore(article, initialBgColor, initialFontSize, () => window.close(), 'reader-newtab-overlay', true); if (originalContentElement) { contentObserver = new MutationObserver(() => { const newArticle = extractContent(); if (newArticle) updateReaderContent(overlay, newArticle); }); contentObserver.observe(originalContentElement, { childList: true, subtree: true, characterData: true }); } 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%'; const settings = loadSettings(); // console.log('[阅读模式] 进入 - 读取设置:', settings); readerOverlay = buildOverlayReaderUI(article, settings.bgColor, settings.fontSize); 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() { const isMenuTriggered = new URLSearchParams(window.location.search).get('reader_trigger') === 'menu'; let attempts = 0; const maxAttempts = 5; const tryExtract = () => { const article = extractContent(); if (article) { originalUrl = window.location.href.replace(/#.*$/, ''); const settings = loadSettings(); // console.log('[阅读模式] 新标签页 - 读取设置:', settings); buildNewTabReaderUI(article, settings.bgColor, settings.fontSize); updateMenuCommands(); } else { attempts++; if (attempts < maxAttempts) setTimeout(tryExtract, 300); else if (isMenuTriggered) { alert('未找到可阅读的正文内容,可能加载超时'); window.close(); } } }; tryExtract(); } function openReaderInNewTab() { window.open(addReaderModeHash(window.location.href), '_blank'); } // ==================== 油猴菜单管理 ==================== function updateMenuCommands() { menuCommandIds.forEach(id => { try { GM_unregisterMenuCommand(id); } catch(e) {} }); menuCommandIds = []; if (SHOULD_ACTIVATE_NEW_TAB) { menuCommandIds.push(GM_registerMenuCommand('❌ 退出阅读模式', () => window.close())); } else if (isReaderMode) { menuCommandIds.push(GM_registerMenuCommand('❌ 退出阅读模式', exitLegacyReaderMode)); } else { menuCommandIds.push(GM_registerMenuCommand('打开阅读模式', enterLegacyReaderMode)); menuCommandIds.push(GM_registerMenuCommand('新标签页打开阅读模式', openReaderInNewTab)); } } // ==================== 初始化 ==================== function init() { if (SHOULD_ACTIVATE_NEW_TAB) { activateNewTabReaderMode(); } else { let retryCount = 0; const maxRetries = 5; const intervalId = setInterval(() => { const testArticle = extractContent(); retryCount++; if (testArticle || retryCount >= maxRetries) { clearInterval(intervalId); if (testArticle) createMainButtonOnce(); updateMenuCommands(); } }, 300); } } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();