// ==UserScript== // @name 阅读模式·纯净版 // @namespace https://viayoo.com/hqzw7k // @version 21.0.0 // @description 在当前页面之上叠加一层纯净阅读界面,原页面冻结于下方,退出后自动恢复。自动识别正文、标题、翻页链接及目录,过滤视频、样式、脚本、按钮、广告等干扰。支持背景色、字号、夜间模式、动态切章(无刷新)、下一章快速切换、目录抽屉、进度条跳章、新标签页打开。 // @author DeepSeek // @homepageURL https://scriptcat.org/zh-CN/users/184036 // @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', '.file_content', '.submenu + .con', '#article', '#articleBody', '#article-body', '#post-content', '#entry-content', '#main-content', '#content-main', '#post', '.article-body', '.post-content', '.entry-content', '.post-body', '.entry-body', '.article-content', '.content-inner', '.articleText', '#js_content', '#baikan-content', '.news-content', '.rm_txt_con', '#reader-container', '.rich_media_content', '#detail', '.detail_nr', '#chapterDiv', '.article_content', '#contentText', '.fallback-content', '#mw-content-text', '#content', '.article', '[itemprop="articleBody"]', '[itemprop="text"]', '[role="article"]', '.readable-content', '.post-article', '.markdown-body', 'article', 'main', '[role="main"]', '.post', '.blog-post', '.story', '.content-wrapper' ]; 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 INDEX_URL_HINTS = /indexlist|chapterlist|catalog|mulu|list/i; const EXTRA_REMOVE_SELECTORS = [ '.aritcle_card', '.projects-header', '.card-list', '.apd-bg', '#comments', '.comments', '.url-card', '.commits-part', '.fold_arrow', '.apd' ]; const EXTRA_REMOVE_TEXTS = [ '举报', '回复' ]; const EXTRA_REMOVE_TEXT_PATTERNS = [ '第*章' ]; const INTERACTIVE_SELECTOR = 'a, img, button, summary, details, input, label, select, textarea, video, audio, [onclick], [role="button"], [contenteditable="true"]'; const SHOULD_ACTIVATE_NEW_TAB = window.location.hash === '#readermode'; const ICONS = { back: ``, open: ``, catalog: ``, settings: ``, moon: ``, sun: ``, crosshair: ``, prev: ``, next: ``, bg: ``, translate: ``, catalogSortAsc: ``, catalogSortDesc: ``, missing: `` }; const STORAGE_KEYS = { BG_COLOR: 'reader_bg_color', FONT_SIZE: 'reader_font_size', THEME_MODE: 'reader_theme_mode', }; const DEFAULT_SETTINGS = { bgColor: '#ffffff', fontSize: '18px', themeMode: 'day', }; const MIN_CONTENT_LENGTH = 50; 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), themeMode: normalizeThemeMode(gmGet(STORAGE_KEYS.THEME_MODE, DEFAULT_SETTINGS.themeMode)) }; } function normalizeThemeMode(mode) { return mode === 'night' ? 'night' : 'day'; } function saveSetting(key, value) { gmSet(key, value); } let toastHost = null; let toastShadow = null; let toastContainer = null; function ensureToastHost() { if (toastHost && toastShadow && toastContainer && document.documentElement.contains(toastHost)) return; toastHost = document.createElement('div'); toastHost.id = 'reader-toast-host'; toastHost.style.cssText = 'all:initial;position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;pointer-events:none;'; toastShadow = toastHost.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = ` .toast-container { position:fixed; left:0; right:0; bottom:75px; display:flex; flex-direction:column; align-items:center; gap:8px; pointer-events:none; z-index:2147483647; } .toast-item { background:rgba(28,28,30,0.85); color:#fff; padding:14px 24px; border-radius:999px; font-size:14px; font-weight:500; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; box-shadow:0 8px 24px rgba(0,0,0,0.2); animation:toastIn 0.3s cubic-bezier(0.22,1,0.36,1); opacity:0; transform:translateY(20px); animation-fill-mode:forwards; max-width:80%; text-align:center; word-break:break-word; } .toast-item.out { animation:toastOut 0.3s cubic-bezier(0.22,1,0.36,1) forwards; } @keyframes toastIn { from { opacity:0; transform:translateY(20px); } to { opacity:1; transform:translateY(0); } } @keyframes toastOut { from { opacity:1; transform:translateY(0); } to { opacity:0; transform:translateY(20px); } } `; toastShadow.appendChild(style); toastContainer = document.createElement('div'); toastContainer.className = 'toast-container'; toastShadow.appendChild(toastContainer); document.documentElement.appendChild(toastHost); } function showShadowToast(msg, duration = 2000) { try { ensureToastHost(); const item = document.createElement('div'); item.className = 'toast-item'; item.textContent = msg; toastContainer.appendChild(item); setTimeout(() => { item.classList.add('out'); setTimeout(() => { try { item.remove(); } catch(e) {} }, 320); }, duration); } catch(e) {} } function notify(msg) { if (window.via && typeof window.via.toast === 'function') { try { window.via.toast(msg); return; } catch(e) {} } showShadowToast(msg); } function normalizeUrl(u) { try { const url = new URL(u, window.location.href); return url.origin + url.pathname; } catch(e) { return u; } } let isReaderMode = false; let buttonHideTimeout = null; let mainButton = null; let originalUrl = ''; let menuCommandIds = []; let readerOverlay = null; let newTabOverlay = null; let originalBodyOverflow = ''; let originalBodyPosition = ''; let originalBodyTop = ''; let originalBodyWidth = ''; let scrollY = 0; let currentCatalogView = null; let catalogCache = null; let catalogCacheLoading = null; let chapterCache = new Map(); let currentPreloadTask = null; let preloadScheduleTimer = null; let catalogOrder = 'asc'; const config = { backgroundOptions: { '默认白': '#ffffff', '豆沙绿': '#c7edcc', '护眼绿': '#e3f2e1', '藕粉': '#f5e6e8', '护眼黑': '#1a1a1a' } }; const NIGHT_THEME = { bgColor: '#111d2b', textColor: '#c0c5cc', borderColor: '#2a3b4d', controlBgColor: '#172535', controlBorderColor: '#2a3b4d', subtleTextColor: '#8892a0', exitBtnColor: '#8892a0', navBtnColor: '#4a9eff', codeBg: 'rgba(255,255,255,0.05)', codeBorder: 'rgba(255,255,255,0.18)', codeTextColor: '#e6edf3', inlineCodeBg: 'rgba(255,255,255,0.12)', tableHeaderBg: 'rgba(255,255,255,0.06)', tableBorder: 'rgba(255,255,255,0.18)' }; 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 splitBrParagraphs(root) { const BLOCK_TAGS = new Set([ 'DIV', 'P', 'UL', 'OL', 'TABLE', 'BLOCKQUOTE', 'PRE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'SECTION', 'ARTICLE', 'DL', 'FIGURE', 'FORM', 'HEADER', 'FOOTER', 'NAV', 'MAIN', 'ASIDE', 'DETAILS' ]); const allDivs = Array.from(root.querySelectorAll('div')); for (const block of allDivs) { try { processBlock(block); } catch(e) {} } function processBlock(block) { const nodes = Array.from(block.childNodes); const hasContent = nodes.some(n => (n.nodeType === 3 && (n.nodeValue || '').trim().length > 0) || (n.nodeType === 1 && n.tagName === 'BR') ); if (!hasContent) return; const rawText = (block.textContent || '').trim(); if (rawText.length < 50) return; const groups = []; let current = []; let brCount = 0; const flushCurrent = () => { if (current.length > 0) { groups.push({ type: 'p', nodes: current }); current = []; } }; for (const node of nodes) { if (node.nodeType === 1 && node.tagName === 'BR') { brCount++; if (brCount >= 2) { flushCurrent(); brCount = 0; } else { current.push(node); } } else if (node.nodeType === 1 && BLOCK_TAGS.has(node.tagName)) { flushCurrent(); groups.push({ type: 'keep', node }); brCount = 0; } else if (node.nodeType === 1 || node.nodeType === 3) { brCount = 0; current.push(node); } } flushCurrent(); const pGroups = groups.filter(g => g.type === 'p'); if (pGroups.length === 0) return; const totalPText = pGroups.reduce((s, g) => { return s + g.nodes.reduce((s2, n) => s2 + (n.textContent || '').replace(/\s+/g, '').length, 0); }, 0); if (totalPText < 50) return; while (block.firstChild) block.removeChild(block.firstChild); for (const group of groups) { if (group.type === 'keep') { block.appendChild(group.node); } else { const p = document.createElement('p'); for (const n of group.nodes) p.appendChild(n); if (p.textContent.trim().length > 0 || p.querySelector('img')) { block.appendChild(p); } } } } } function wrapLooseTextNodes(root) { const BLOCK_TAGS = new Set([ 'DIV', 'P', 'UL', 'OL', 'TABLE', 'BLOCKQUOTE', 'PRE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'SECTION', 'ARTICLE', 'DL', 'FIGURE', 'FORM', 'HEADER', 'FOOTER', 'NAV', 'MAIN', 'ASIDE', 'DETAILS', 'HR' ]); const divs = Array.from(root.querySelectorAll('div')); for (const div of divs) { try { const children = Array.from(div.childNodes); if (children.some(n => n.nodeType === 1 && n.tagName === 'P')) continue; const hasBlockChild = children.some(n => n.nodeType === 1 && BLOCK_TAGS.has(n.tagName) ); if (!hasBlockChild) continue; const hasLooseText = children.some(n => n.nodeType === 3 && (n.nodeValue || '').trim().length > 0 ); if (!hasLooseText) continue; const newChildren = []; let buffer = []; const flush = () => { if (buffer.length === 0) return; const hasContent = buffer.some(n => { if (n.nodeType === 3) return (n.nodeValue || '').trim().length > 0; return true; }); if (!hasContent) { buffer = []; return; } const p = document.createElement('p'); for (const n of buffer) { if (n.nodeType === 3) { const parts = (n.nodeValue || '').split('\n'); for (let i = 0; i < parts.length; i++) { if (i > 0) p.appendChild(document.createElement('br')); if (parts[i]) p.appendChild(document.createTextNode(parts[i])); } } else { p.appendChild(n); } } newChildren.push(p); buffer = []; }; for (const node of children) { if (node.nodeType === 1 && BLOCK_TAGS.has(node.tagName)) { flush(); newChildren.push(node); } else { buffer.push(node); } } flush(); while (div.firstChild) div.removeChild(div.firstChild); for (const n of newChildren) div.appendChild(n); } catch(e) {} } } function buildTextPatternRegex(pattern) { const parts = pattern.split('*').map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); const body = parts.join('[\\d一二三四五六七八九十百千万零两]+'); return new RegExp('^' + body + '(\\s|$|[::.、\\-—])'); } function cleanContent(c, shouldSplit) { c.querySelectorAll('[style*="display: none"], [style*="display:none"], [style*="visibility: hidden"], [style*="visibility:hidden"]').forEach(e => e.remove()); c.querySelectorAll('markdown-accessiblity-table').forEach(el => { const p = el.parentNode; if (p) { while (el.firstChild) p.insertBefore(el.firstChild, el); p.removeChild(el); } }); for (const s of EXTRA_REMOVE_SELECTORS) c.querySelectorAll(s).forEach(e => e.remove()); if (EXTRA_REMOVE_TEXTS.length) { const esc = t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const exact = new Set(EXTRA_REMOVE_TEXTS); const suffixRe = new RegExp('^(' + EXTRA_REMOVE_TEXTS.map(esc).join('|') + ')[((]\\d*[))]?$'); c.querySelectorAll('a').forEach(a => { const t = (a.textContent || '').trim(); if (exact.has(t) || suffixRe.test(t)) a.remove(); }); } if (EXTRA_REMOVE_TEXT_PATTERNS.length) { const patternRes = EXTRA_REMOVE_TEXT_PATTERNS.map(buildTextPatternRegex); c.querySelectorAll('h1, h2, h3, h4, h5, h6, p').forEach(el => { const t = (el.textContent || '').trim(); if (t && patternRes.some(re => re.test(t))) el.remove(); }); } 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); c.querySelectorAll('i').forEach(e => { if (!e.textContent.trim() && !e.querySelector('img')) e.remove(); }); c.querySelectorAll('figure.highlight, div.highlight, figure[class*="highlight"], div[class*="highlight"]').forEach(container => { const pre = container.querySelector('pre'); if (!pre) return; const table = container.querySelector('table'); if (!table) return; const tableText = (table.textContent || '').replace(/\s+/g, ''); const preText = (pre.textContent || '').replace(/\s+/g, ''); if (tableText !== preText) return; const preClone = pre.cloneNode(true); while (container.firstChild) container.removeChild(container.firstChild); container.appendChild(preClone); }); c.querySelectorAll('*').forEach(e => { e.removeAttribute('style'); e.removeAttribute('class'); }); ['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); } }); }); 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); } }); c.querySelectorAll('img').forEach(img => { const src = img.getAttribute('src') || img.getAttribute('data-original') || img.getAttribute('data-src') || img.getAttribute('data-lazy-src') || img.getAttribute('data-echo') || ''; const rawClass = img.getAttribute('class') || ''; const isEmoji = /emotion|emoji/i.test(src) || /emoji/i.test(rawClass); const altVal = img.getAttribute('alt') || ''; const widthVal = img.getAttribute('width') || ''; const heightVal = img.getAttribute('height') || ''; const attrs = img.attributes; for (let i = attrs.length - 1; i >= 0; i--) { img.removeAttribute(attrs[i].name); } if (altVal) img.setAttribute('alt', altVal); if (src) { img.setAttribute('src', src); if (isEmoji) { img.setAttribute('data-emoji', '1'); } else { if (widthVal) img.setAttribute('width', widthVal); if (heightVal) img.setAttribute('height', heightVal); } } else { img.remove(); } }); if (shouldSplit) splitBrParagraphs(c); wrapLooseTextNodes(c); stripLeadingSpaces(c); } function detectIndent(el) { const blocks = el.querySelectorAll('p, li, div'); if (blocks.length > 0) { let cnt = 0; const n = Math.min(blocks.length, 10); for (let i = 0; i < n; i++) { const ti = parseFloat(window.getComputedStyle(blocks[i]).textIndent) || 0; if (ti > 1) cnt++; } if (cnt > n / 2) return true; } const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null); let textCount = 0, indentCount = 0, node; while ((node = walker.nextNode()) !== null && textCount < 30) { const text = node.nodeValue || ''; if (text.trim().length < 5) continue; textCount++; if (/^(?:\u3000\u3000|\u3000|\s{3,})/.test(text)) indentCount++; } return textCount > 0 && indentCount > textCount * 0.5; } function findLinkByText(doc, 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 ? doc.querySelectorAll(base) : doc.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 = doc.querySelector(s); if (el && el.tagName === 'A' && el.href && isValidHref(el.href)) return el; } } return null; } function collectLinksByText(doc, selectors) { const results = []; const seen = new Set(); 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\(.+?\)/, ''); let els; try { els = base ? doc.querySelectorAll(base) : doc.querySelectorAll('a'); } catch(e) { continue; } for (const el of els) { if (el.tagName === 'A' && el.href && el.textContent.includes(text) && isValidHref(el.href)) { const key = el.href; if (!seen.has(key)) { seen.add(key); results.push(el); } } } } } else { let els; try { els = doc.querySelectorAll(s); } catch(e) { continue; } for (const el of els) { if (el.tagName === 'A' && el.href && isValidHref(el.href)) { const key = el.href; if (!seen.has(key)) { seen.add(key); results.push(el); } } } } } return results; } function findNavLinksByContext(doc) { 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 = doc.evaluate(xpaths[key], doc, 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 = doc.evaluate(`//*[contains(text(),"${txt}")]`, doc, 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(doc) { doc = doc || document; let links = { prev: findLinkByText(doc, NAV_SELECTORS.prev), next: findLinkByText(doc, NAV_SELECTORS.next), index: findLinkByText(doc, NAV_SELECTORS.index) }; const cl = findNavLinksByContext(doc); 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; try { const indexCandidates = collectLinksByText(doc, NAV_SELECTORS.index); if (cl.index && !indexCandidates.some(a => a.href === cl.index.href)) { indexCandidates.push(cl.index); } if (indexCandidates.length > 0) { indexCandidates.sort((a, b) => { const aHit = INDEX_URL_HINTS.test(a.href) ? 0 : 1; const bHit = INDEX_URL_HINTS.test(b.href) ? 0 : 1; return aHit - bHit; }); const best = indexCandidates[0]; if (best && best.href && isValidHref(best.href)) { if (!links.index || links.index.href !== best.href) { // console.log(`[阅读模式] 目录优选:主目录 ${links.index ? links.index.href : '(无)'} → ${best.href}`); } links.index = best; } } links.indexCandidates = indexCandidates.map(a => a.href); } catch(e) {} return links; } function extractContent(doc) { doc = doc || document; let contentElement = null; for (const s of CONTENT_SELECTORS) { const els = doc.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; const autoHasIndent = detectIndent(contentElement); const clone = contentElement.cloneNode(true); cleanContent(clone, autoHasIndent); const html = clone.innerHTML.trim(); const textContent = clone.textContent.trim(); if (!html || !textContent || textContent.length <= MIN_CONTENT_LENGTH) return null; let title = ''; for (const s of TITLE_SELECTORS) { const el = doc.querySelector(s); if (el && el.textContent.trim()) { if (el.querySelector('a')) { const cloneEl = el.cloneNode(true); cloneEl.querySelectorAll('a').forEach(a => a.remove()); title = cloneEl.textContent .replace(/^[\s>»›\-—::]+/, '') .trim(); } else { title = el.textContent.trim(); } // console.log(`阅读模式: 使用标题选择器 "${s}" 匹配到标题: "${title}"`); break; } } if (!title) { title = doc.title || ''; // console.log(`阅读模式: 使用网页标题: "${title}"`); } return { title, content: html, nav: findNavLinks(doc), autoHasIndent }; } 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 isDarkColor(bg) { if (!bg) return false; let r, g, b; if (bg.startsWith('#') && (bg.length === 7 || bg.length === 4)) { let hex = bg.slice(1); if (hex.length === 3) hex = hex.split('').map(c => c + c).join(''); r = parseInt(hex.slice(0, 2), 16); g = parseInt(hex.slice(2, 4), 16); b = parseInt(hex.slice(4, 6), 16); } else if (bg.startsWith('rgb')) { const m = bg.match(/\d+/g); if (!m || m.length < 3) return false; r = parseInt(m[0], 10); g = parseInt(m[1], 10); b = parseInt(m[2], 10); } else { return false; } const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance < 0.5; } function calculateColors(bg, isNight) { if (isNight) { return { textColor: NIGHT_THEME.textColor, borderColor: NIGHT_THEME.borderColor, controlBgColor: NIGHT_THEME.controlBgColor, controlBorderColor: NIGHT_THEME.controlBorderColor, subtleTextColor: NIGHT_THEME.subtleTextColor, exitBtnColor: NIGHT_THEME.exitBtnColor, navBtnColor: NIGHT_THEME.navBtnColor, codeBg: NIGHT_THEME.codeBg, codeBorder: NIGHT_THEME.codeBorder, codeTextColor: NIGHT_THEME.codeTextColor, inlineCodeBg: NIGHT_THEME.inlineCodeBg, tableHeaderBg: NIGHT_THEME.tableHeaderBg, tableBorder: NIGHT_THEME.tableBorder }; } const dark = isDarkColor(bg); return { textColor: dark ? '#e0e0e0' : '#000', borderColor: dark ? '#444' : '#eee', controlBgColor: bg, 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 getEffectiveBgColor(userBg, isNight) { return isNight ? NIGHT_THEME.bgColor : userBg; } function getAccentColor(userBg, isNight) { if (isNight) return NIGHT_THEME.navBtnColor; return isDarkColor(userBg) ? '#4a9eff' : '#007bff'; } function getIndentRule(autoHasIndent) { return autoHasIndent ? '#reader-content-section p, #reader-content-section li{text-indent:2em;}' : '#reader-content-section p, #reader-content-section li{text-indent:0;}'; } function buildContentStyleCSS(userBg, autoHasIndent, isNight) { const colors = calculateColors(userBg, isNight); const indentRule = getIndentRule(autoHasIndent); 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; word-break: break-all; box-sizing: border-box; } #reader-content-section p { text-align: justify; margin: 0 0 1.8em 0; white-space: normal !important; } ${indentRule} #reader-content-section img { max-width: 100%; height: auto; } #reader-content-section img[data-emoji] { display: inline; max-width: none; width: 1.2em; height: 1.2em; vertical-align: -0.15em; } #reader-content-section p > img:only-child, #reader-content-section div > img:only-child { display: block; margin: 1.2em 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; margin-top: 2em; margin-bottom: 0.8em; } #reader-content-section h3 { font-size: 1.3em; } #reader-content-section ul, #reader-content-section ol { padding-left: 0; margin: 0.5em 0; list-style: none; } #reader-content-section li { display: block; list-style: none; margin: 0.2em 0; padding-left: 1.2em; position: relative; } #reader-content-section ul > li::before { content: ''; position: absolute; left: 0.2em; top: 0.85em; width: 5px; height: 5px; border-radius: 50%; background: currentColor; } #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 chineseNumToArabic(s) { if (!s) return null; if (/^\d+$/.test(s)) return parseInt(s, 10); const digitMap = { '零': 0, '一': 1, '二': 2, '两': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9 }; const unitMap = { '十': 10, '百': 100, '千': 1000 }; let total = 0; let cur = 0; for (const ch of s) { if (digitMap[ch] !== undefined) { cur = cur * 10 + digitMap[ch]; } else if (unitMap[ch] !== undefined) { const u = unitMap[ch]; if (cur === 0) cur = 1; total += cur * u; cur = 0; } } total += cur; return total > 0 ? total : null; } function extractNumFromTitle(text) { if (!text) return null; const m = text.match(/第\s*([0-90-9]+|[一二三四五六七八九十百千万零两]+)\s*[章节回卷话集]/); if (!m) return null; return chineseNumToArabic(m[1]); } function sortChapterLinksByOrder(links, extractNumFromHref) { if (!links || links.length < 2) return links; let titleHit = 0, total = 0; const titleNums = new Map(); for (const l of links) { if (l.__missing) continue; total++; const n = extractNumFromTitle(l.text); if (n !== null) { titleNums.set(l, n); titleHit++; } } const titleReliable = total > 0 && titleHit >= total * 0.8; const entries = []; for (let i = 0; i < links.length; i++) { const l = links[i]; if (l.__missing) { if (l._num !== null && l._num !== undefined) { entries.push({ pos: i, key: l._num, item: l }); } continue; } if (titleReliable) { const tn = titleNums.get(l); if (tn !== null && tn !== undefined) { entries.push({ pos: i, key: tn, item: l }); } } else { const un = extractNumFromHref(l.href); if (un !== null && un !== undefined) { entries.push({ pos: i, key: un, item: l }); } } } if (entries.length < 2) return links; const sorted = entries.slice().sort((a, b) => a.key - b.key); let changed = 0; for (let i = 0; i < entries.length; i++) { if (links[entries[i].pos] !== sorted[i].item) changed++; links[entries[i].pos] = sorted[i].item; } if (changed > 0) { // console.log(`[阅读模式] 目录排序:${entries.length} 项带编号章节已按编号排列(无编号章节原位不动)`); } return links; } function extractMissingChapterItems(doc, indexUrl, existingNums, minNum, maxNum) { if (!doc) return []; const results = []; const seenNums = new Set(); const titleRe = /第\s*(?:[0-90-9]+|[一二三四五六七八九十百千万零两]+)\s*[章节回卷话集]/; doc.querySelectorAll('li, dd').forEach(el => { if (el.querySelector('a[href]')) return; const text = (el.textContent || '').trim(); if (!text || text.length < 2 || text.length > 80) return; if (!titleRe.test(text)) return; const num = extractNumFromTitle(text); if (num === null) return; if (num < minNum || num > maxNum) return; if (existingNums.has(num)) return; if (seenNums.has(num)) return; seenNums.add(num); results.push({ href: null, text, __missing: true, _num: num }); }); return results; } function extractChapterLinksFromDoc(doc, indexUrl) { if (!doc) return []; const containerSelectors = [ 'ul.chapter', '#chapterlist', '.chapterlist', '#list', '.listmain', '.chapter', '#chapter', '.book_list', '.booklist', '.volume', '#chapter-list', '.chapter-list', '.mulu', '#mulu', '.catalog', '.catalog-list', '#readerlist', '.list-chapter', '#list-chapter', 'dl#list', 'dl.chapterlist', '.section-list', '.chapters', '#jsList1', '.list_li', '.lb_mulu', '.chapterList' ]; function findBestContainer(root) { let container = null; let maxLinks = 0; for (const sel of containerSelectors) { try { const els = root.querySelectorAll(sel); for (const el of els) { const count = el.querySelectorAll('a').length; if (count > maxLinks) { maxLinks = count; container = el; } } } catch(e) {} } return { container, maxLinks }; } let basePath = ''; let indexOrigin = ''; try { const u = new URL(indexUrl); indexOrigin = u.origin; basePath = u.pathname.replace(/index(_\d+)?\.html?$/i, ''); if (!basePath.endsWith('/')) { basePath = basePath.replace(/[^/]+$/, ''); } } catch(e) {} const excludeTexts = [ '上一页', '下一页', '上一章', '下一章', '首页', '返回', '目录', '书架', '顶部', '底部', '开始阅读', '加入书架', '存书签', '我的书架', '阅读记录', '回到顶部', '章节目录', '返回目录', '返回书页', '直达底部', '点击阅读', '换手', '关灯', '倒序', '字体', '大', '中', '小', '正序', '升序', '降序', '顺序', '逆序', '排序', '展开', '收起', '展开全部', '收起全部', '全部章节', '完整目录', '查看全部', '显示全部' ]; const DOWNLOAD_TEXT_RE = /下载|TXT|电子书|全本|手机阅读|epub|mobi/i; const DOWNLOAD_LAST_SEG_RE = /^(?:txt|down|download|dload|xiazai|epub|mobi|pdf|zip|rar)(?:\.html?)?$/i; function isChapterCandidate(a) { const href = a.getAttribute('href'); if (!href || href === '#' || href.startsWith('javascript:')) return false; const text = (a.textContent || '').trim(); if (!text || text.length > 80 || text.length < 2) return false; const normText = text.replace(/^[\[【((]+/, '').replace(/[\]】))]+$/, '').trim(); if (excludeTexts.some(w => text === w || normText === w)) return false; if (/^\d{1,4}$/.test(text)) return false; if (DOWNLOAD_TEXT_RE.test(text)) return false; try { const absObj = new URL(href, indexUrl); const lastSeg = absObj.pathname.split('/').pop() || ''; if (DOWNLOAD_LAST_SEG_RE.test(lastSeg)) return false; } catch(e) {} return true; } function toAbs(a) { const href = a.getAttribute('href'); try { return new URL(href, indexUrl).href; } catch(e) { return null; } } function extractNumericKey(u) { try { const urlObj = new URL(u); const m = urlObj.pathname.match(/(\d+)(?=\.html?$)/i); if (m) return parseInt(m[1], 10); const m2 = urlObj.pathname.match(/(\d+)/g); if (m2 && m2.length) return parseInt(m2[m2.length - 1], 10); } catch(e) {} return null; } function scoreOfText(t) { if (!t) return 0; let s = 0; if (/第\s*[\d一二三四五六七八九十百千万零两]+\s*[章节回卷话集]/.test(t)) s += 100; if (/chapter\s*\d+/i.test(t)) s += 100; if (/\d/.test(t)) s += 10; s += Math.min(t.length, 60); return s; } function pathSig(href) { try { const u = new URL(href); const p = u.pathname.replace(/\d+/g, '#').replace(/\/+/g, '/'); return p + (u.search ? '?' + u.search.replace(/\d+/g, '#') : ''); } catch(e) { return href; } } function clusterByPathPattern(links) { if (!links || links.length < 5) return links; const groups = new Map(); for (const l of links) { if (!l || l.__missing) continue; const s = pathSig(l.href); let arr = groups.get(s); if (!arr) { arr = []; groups.set(s, arr); } arr.push(l); } if (groups.size <= 1) return links; let bestPattern = null; let bestSize = 0; for (const [s, arr] of groups) { if (arr.length > bestSize) { bestSize = arr.length; bestPattern = s; } } if (bestSize >= 5 && bestSize >= links.length * 0.6) { const kept = links.filter(l => l.__missing || pathSig(l.href) === bestPattern); if (kept.length < links.length) { // console.log(`[阅读模式] 路径模式聚类:${links.length} → ${kept.length} 条`); } return kept; } return links; } function finalizeResult(result) { if (!result.length) return result; result = clusterByPathPattern(result); const existingNums = new Set(); for (const r of result) { const n = extractNumericKey(r.href); if (n !== null) existingNums.add(n); } if (existingNums.size >= 5) { const numsArr = Array.from(existingNums).sort((a, b) => a - b); const minN = numsArr[0]; const maxN = numsArr[numsArr.length - 1]; try { const missing = extractMissingChapterItems(doc, indexUrl, existingNums, minN, maxN); if (missing.length > 0) { for (const m of missing) result.push(m); } } catch(e) {} } sortChapterLinksByOrder(result, extractNumericKey); return result; } if (basePath && indexOrigin) { const normBase = basePath.endsWith('/') ? basePath : basePath + '/'; const { container: preContainer, maxLinks: preMax } = findBestContainer(doc); const scanRoot = (preContainer && preMax >= 5) ? preContainer : doc; if (scanRoot !== doc) { // console.log(`[阅读模式] 路径前缀策略:在容器内扫描(${preMax} 条 a)`); } const allLinks = Array.from(scanRoot.querySelectorAll('a[href]')); const seen = new Map(); const result = []; for (const a of allLinks) { if (!isChapterCandidate(a)) continue; const abs = toAbs(a); if (!abs) continue; let absObj; try { absObj = new URL(abs); } catch(e) { continue; } if (absObj.origin !== indexOrigin) continue; if (!absObj.pathname.startsWith(normBase)) continue; if (/index(_\d+)?\.html?$/i.test(absObj.pathname)) continue; if (absObj.pathname === normBase || absObj.pathname === basePath) continue; const text = (a.textContent || '').trim(); if (seen.has(abs)) { const idx = seen.get(abs); if (scoreOfText(text) > scoreOfText(result[idx].text)) { result[idx].text = text; } continue; } seen.set(abs, result.length); result.push({ href: abs, text }); } if (result.length >= 5) { // console.log(`[阅读模式] 目录:基于路径前缀抓取到 ${result.length} 条章节链接`); sortChapterLinksByOrder(result, extractNumericKey); return finalizeResult(result); } } let { container, maxLinks } = findBestContainer(doc); if (!container || maxLinks < 5) { const candidates = Array.from(doc.querySelectorAll('ul, ol, div, article, section, dl, table')); let best = null, bestScore = 0; for (const c of candidates) { const links = c.querySelectorAll('a[href]'); if (links.length < 5) continue; let score = 0; for (const a of links) { const href = a.getAttribute('href') || ''; if (basePath && href.startsWith(basePath)) score += 3; else score += 1; } if (score > bestScore) { bestScore = score; best = c; } } if (best) container = best; } const rawLinks = container ? Array.from(container.querySelectorAll('a')) : Array.from(doc.querySelectorAll('a')); const seen = new Map(); const result = []; for (const a of rawLinks) { if (!isChapterCandidate(a)) continue; const abs = toAbs(a); if (!abs) continue; const text = (a.textContent || '').trim(); if (seen.has(abs)) { const idx = seen.get(abs); if (scoreOfText(text) > scoreOfText(result[idx].text)) { result[idx].text = text; } continue; } seen.set(abs, result.length); result.push({ href: abs, text }); } if (basePath && result.length > 0) { const sameDir = result.filter(item => { try { return new URL(item.href).pathname.startsWith(basePath); } catch(e) { return false; } }); if (sameDir.length >= 5 && sameDir.length >= result.length * 0.5) { // console.log(`[阅读模式] 目录同路径过滤:${sameDir.length}/${result.length} 条`); return finalizeResult(sameDir); } } return finalizeResult(result); } function safeGetIframeDoc(iframe) { try { const doc = iframe.contentDocument; if (doc) return { doc, error: null }; if (iframe.contentWindow) { return { doc: iframe.contentWindow.document, error: null }; } return { doc: null, error: 'empty' }; } catch (e) { if (e.name === 'SecurityError') { return { doc: null, error: 'cross-origin' }; } return { doc: null, error: 'unknown' }; } } function preloadCatalog(indexUrl, overlay) { if (catalogCache && catalogCache.url === indexUrl) return; if (catalogCacheLoading && catalogCacheLoading.url === indexUrl) return; catalogCacheLoading = { url: indexUrl }; let finalized = false; const finalize = (links) => { if (finalized) return; finalized = true; catalogCache = { url: indexUrl, links: links || [] }; catalogCacheLoading = null; if (links && links.length > 0) { // console.log(`[阅读模式] 目录预加载成功,共 ${links.length} 条章节链接`); } else { // console.error('[阅读模式] 目录预加载失败: 未抓取到章节链接'); } if (overlay) overlay.dispatchEvent(new CustomEvent('reader-catalog-ready')); }; const hardTimeout = setTimeout(() => finalize([]), 60000); function extractNum(u) { try { const p = new URL(u).pathname; const m = p.match(/\/(\d+)\.html?$/i) || p.match(/(\d+)(?=\.html?$)/i); return m ? parseInt(m[1], 10) : null; } catch(e) { return null; } } function loadDocTemp(url, timeoutMs) { return new Promise((resolve) => { const iframe = document.createElement('iframe'); iframe.style.cssText = 'position:absolute;width:0;height:0;border:none;visibility:hidden;'; iframe.src = url; let done = false; const cleanup = (result) => { if (done) return; done = true; try { if (iframe.parentNode) iframe.remove(); } catch(e) {} resolve(result); }; const t = setTimeout(() => cleanup(null), timeoutMs || 12000); iframe.onload = () => { clearTimeout(t); const result = safeGetIframeDoc(iframe); cleanup(result.doc); }; document.body.appendChild(iframe); }); } function findPagerUrls(doc, baseUrl) { if (!doc) return []; const found = new Set(); const baseKey = normalizeUrl(baseUrl); const pathBase = (() => { try { const u = new URL(baseUrl); const p = u.pathname; if (p.endsWith('/')) return p; return p.replace(/[^/]+$/, ''); } catch(e) { return '/'; } })(); const tryRoot = (root) => { if (!root || !root.querySelectorAll) return; root.querySelectorAll('a[href]').forEach(a => { const href = a.getAttribute('href'); if (!href || href === '#' || href.startsWith('javascript:')) return; if (!isValidHref(href)) return; try { const absUrl = new URL(href, baseUrl); const abs = absUrl.href; if (normalizeUrl(abs) === baseKey) return; if (!absUrl.pathname.startsWith(pathBase)) return; const path = absUrl.pathname; const search = absUrl.search; if (/\/index_\d+\.html?$/i.test(path) || /\/page[-_]?\d+\.html?$/i.test(path) || /\/list[-_]?\d+\.html?$/i.test(path) || /[?&]page=\d+/i.test(search)) { found.add(abs); } } catch(e) {} }); }; const containers = [ '.pages', '.pagination', '.page-list', '.pagelist', '.pager', '.page-nav', '.page_nav', 'nav.pagination', '.listpage', '.paginate', '.wp-pagenavi', '#pages', '#pagination', '#pagenavi', '#pagelink', '.pagelink' ]; let hitInContainer = 0; for (const sel of containers) { try { doc.querySelectorAll(sel).forEach(el => { const before = found.size; tryRoot(el); if (found.size > before) hitInContainer++; }); } catch(e) {} } if (hitInContainer === 0 && doc.body) { tryRoot(doc.body); } return Array.from(found); } function detectExpandNeed(doc) { const partialContainers = [ '.catalog-all', '.all-chapter', '.show-all', '.show-more', '.more-chapter', '.cfirst', '.latest', '.recent', '.newest', '.chapter-more', '[class*="cfirst"]', '[class*="latest"]', '[class*="recent"]', '[class*="newest"]', '[class*="all-chapter"]', '[class*="show-all"]' ]; let hasPartialContainer = false; for (const sel of partialContainers) { try { if (doc.querySelector(sel)) { hasPartialContainer = true; break; } } catch(e) {} } const triggerRe = /查看完整|完整目录|完整章节|全部章节|全部目录|展开目录|展开全部|展开更多|显示全部|显示更多|显示完整|加载更多|更多章节|更多目录|查看全部|点击展开|查看所有|所有章节|全部章節|展開目錄|展開全部|加載更多|更多章節|顯示全部/; const triggerEls = []; const seen = new Set(); doc.querySelectorAll('a, button, div, span, li, input[type="button"], input[type="submit"]').forEach(el => { if (seen.has(el)) return; const t = (el.textContent || '').trim(); if (t && t.length <= 30 && triggerRe.test(t)) { triggerEls.push(el); seen.add(el); } }); for (const sel of partialContainers) { try { doc.querySelectorAll(sel).forEach(el => { if (!seen.has(el)) { triggerEls.push(el); seen.add(el); } }); } catch(e) {} } return { needsExpand: hasPartialContainer || triggerEls.length > 0, triggerEls }; } function dispatchRealClick(el) { if (!el || !el.dispatchEvent) return; const doc = el.ownerDocument; const win = doc && doc.defaultView; const base = { bubbles: true, cancelable: true, view: win || undefined }; try { el.dispatchEvent(new PointerEvent('pointerdown', base)); } catch(e) {} try { el.dispatchEvent(new MouseEvent('mousedown', base)); } catch(e) {} try { el.dispatchEvent(new PointerEvent('pointerup', base)); } catch(e) {} try { el.dispatchEvent(new MouseEvent('mouseup', base)); } catch(e) {} try { el.click(); } catch(e) {} } async function runPagerStrategy(pagerUrls, initialLinks) { const allUrls = [indexUrl]; const seenUrls = new Set([normalizeUrl(indexUrl)]); for (const u of pagerUrls) { const k = normalizeUrl(u); if (!seenUrls.has(k)) { seenUrls.add(k); allUrls.push(u); } } const collectedLinks = []; const seenHrefs = new Map(); const pushLinks = (links) => { for (const l of links) { const k = l.__missing ? ('__missing__' + l._num) : normalizeUrl(l.href); if (!seenHrefs.has(k)) { seenHrefs.set(k, collectedLinks.length); collectedLinks.push(l); } } }; pushLinks(initialLinks); const MAX_CONCURRENT = 3; let nextIdx = 1; const workers = []; for (let w = 0; w < MAX_CONCURRENT; w++) { workers.push((async () => { while (!finalized) { const myIdx = nextIdx++; if (myIdx >= allUrls.length) return; const url = allUrls[myIdx]; const doc = await loadDocTemp(url, 12000); if (finalized) return; if (doc) { try { const links = extractChapterLinksFromDoc(doc, url); pushLinks(links); } catch(e) {} } } })()); } await Promise.all(workers); if (finalized) return; sortChapterLinksByOrder(collectedLinks, extractNum); clearTimeout(hardTimeout); finalize(collectedLinks); } function runExpandStrategy(doc, triggerEls, iframe, initialLinks) { const fireClicks = () => { for (const el of triggerEls) { let cur = el; let depth = 0; while (cur && depth < 3) { try { dispatchRealClick(cur); } catch(e) {} cur = cur.parentElement; depth++; } } }; fireClicks(); let pollCount = 0; const MAX_POLLS = 28; const POLL_INTERVAL = 250; const RE_ACTION_EVERY = 2; const tryExtract = () => { if (finalized) return; let newLinks = []; try { newLinks = extractChapterLinksFromDoc(doc, indexUrl); } catch(e) {} if (newLinks.length > initialLinks.length && newLinks.length >= 5) { if (iframe && iframe.parentNode) iframe.remove(); clearTimeout(hardTimeout); finalize(newLinks); return; } pollCount++; if (pollCount < MAX_POLLS) { if (pollCount % RE_ACTION_EVERY === 0) fireClicks(); setTimeout(tryExtract, POLL_INTERVAL); } else { if (iframe && iframe.parentNode) iframe.remove(); clearTimeout(hardTimeout); finalize(newLinks.length > initialLinks.length ? newLinks : initialLinks); } }; setTimeout(tryExtract, POLL_INTERVAL); } const firstIframe = document.createElement('iframe'); firstIframe.style.cssText = 'position:absolute;width:0;height:0;border:none;visibility:hidden;'; firstIframe.src = indexUrl; let firstLoaded = false; const firstTimeout = setTimeout(() => { if (!firstLoaded) { if (firstIframe.parentNode) firstIframe.remove(); clearTimeout(hardTimeout); finalize([]); } }, 15000); firstIframe.onload = () => { firstLoaded = true; clearTimeout(firstTimeout); const result = safeGetIframeDoc(firstIframe); if (!result.doc) { if (firstIframe.parentNode) firstIframe.remove(); clearTimeout(hardTimeout); finalize([]); return; } const doc = result.doc; let firstLinks = []; try { firstLinks = extractChapterLinksFromDoc(doc, indexUrl); } catch(e) {} const pagerUrls = findPagerUrls(doc, indexUrl); if (pagerUrls.length > 0) { if (firstIframe.parentNode) firstIframe.remove(); runPagerStrategy(pagerUrls, firstLinks); return; } const { needsExpand, triggerEls } = detectExpandNeed(doc); if (needsExpand && triggerEls.length > 0 && firstLinks.length < 30) { runExpandStrategy(doc, triggerEls, firstIframe, firstLinks); return; } if (firstIframe.parentNode) firstIframe.remove(); clearTimeout(hardTimeout); finalize(firstLinks); }; document.body.appendChild(firstIframe); } function preloadNextChapter(url) { if (!url) return; const cacheKey = normalizeUrl(url); if (chapterCache.has(cacheKey)) return; if (currentPreloadTask && normalizeUrl(currentPreloadTask.url) === cacheKey) return; cancelCurrentPreload(); const task = { url: url, iframe: null, timeout: null, pollTimeout: null, cancelled: false }; currentPreloadTask = task; const iframe = document.createElement('iframe'); iframe.style.cssText = 'position:absolute;width:0;height:0;border:none;visibility:hidden;'; iframe.src = url; document.body.appendChild(iframe); task.iframe = iframe; const cleanup = () => { if (task.iframe && task.iframe.parentNode) task.iframe.remove(); if (task.timeout) clearTimeout(task.timeout); if (task.pollTimeout) clearTimeout(task.pollTimeout); task.iframe = null; task.timeout = null; task.pollTimeout = null; if (currentPreloadTask === task) currentPreloadTask = null; }; let loaded = false; task.timeout = setTimeout(() => { if (!loaded) cleanup(); }, 15000); iframe.onload = function() { loaded = true; if (task.timeout) { clearTimeout(task.timeout); task.timeout = null; } if (task.cancelled) { cleanup(); return; } const result = safeGetIframeDoc(iframe); if (!result.doc) { cleanup(); return; } let pollCount = 0; const MAX_POLL_COUNT = 15; const POLL_INTERVAL = 200; const tryExtract = () => { if (task.cancelled) { cleanup(); return; } let article = null; try { article = extractContent(result.doc); } catch(e) {} if (article) { chapterCache.set(cacheKey, { article, timestamp: Date.now() }); if (chapterCache.size > 2) { const firstKey = chapterCache.keys().next().value; chapterCache.delete(firstKey); } cleanup(); return; } pollCount++; if (pollCount < MAX_POLL_COUNT) { task.pollTimeout = setTimeout(tryExtract, POLL_INTERVAL); } else { cleanup(); } }; tryExtract(); }; } function cancelCurrentPreload() { if (!currentPreloadTask) return; const task = currentPreloadTask; task.cancelled = true; if (task.iframe && task.iframe.parentNode) task.iframe.remove(); if (task.timeout) clearTimeout(task.timeout); if (task.pollTimeout) clearTimeout(task.pollTimeout); currentPreloadTask = null; } function schedulePreloadNext(overlay) { if (preloadScheduleTimer) { clearTimeout(preloadScheduleTimer); preloadScheduleTimer = null; } preloadScheduleTimer = setTimeout(() => { preloadScheduleTimer = null; const nav = overlay.__nav; if (!nav || !nav.next) return; preloadNextChapter(nav.next.href); }, 500); } function updateProgressBar(overlay, targetUrl) { const thumb = overlay.querySelector('#reader-progress-thumb'); if (!thumb) return; const container = thumb.parentNode; const canDrag = catalogCache && catalogCache.links && catalogCache.links.length > 1; if (container) { container.style.cursor = canDrag ? 'pointer' : 'default'; container.style.opacity = canDrag ? '1' : '0.5'; } if (catalogCacheLoading) return; if (!catalogCache || !catalogCache.links || !catalogCache.links.length) { thumb.style.left = 'calc(100% - 7px)'; return; } const total = catalogCache.links.length; if (total <= 1) { thumb.style.left = 'calc(100% - 7px)'; return; } const url = targetUrl || window.location.href; const currentKey = normalizeUrl(url); let currentIdx = -1; for (let i = 0; i < total; i++) { const l = catalogCache.links[i]; if (!l || l.__missing || !l.href) continue; if (normalizeUrl(l.href) === currentKey) { currentIdx = i; break; } } if (currentIdx !== -1) { const percent = (currentIdx / (total - 1)) * 100; thumb.style.left = `calc(${percent}% - 7px)`; return; } const extractNum = (u) => { try { const p = new URL(u).pathname; const m = p.match(/\/(\d+)\.html?$/i) || p.match(/(\d+)(?=\.html?$)/i); return m ? parseInt(m[1], 10) : null; } catch(e) { return null; } }; const curNum = extractNum(url); let maxNum = 0; for (const l of catalogCache.links) { if (!l || l.__missing || !l.href) continue; const n = extractNum(l.href); if (n !== null && n > maxNum) maxNum = n; } if (curNum !== null && maxNum > 0 && curNum <= maxNum) { const percent = (curNum / maxNum) * 100; thumb.style.left = `calc(${percent}% - 7px)`; return; } thumb.style.left = 'calc(0% - 7px)'; } function removeCatalogImmediately(overlay) { if (currentCatalogView) { if (currentCatalogView.__pollInterval) { clearInterval(currentCatalogView.__pollInterval); currentCatalogView.__pollInterval = null; } currentCatalogView.remove(); currentCatalogView = null; } const mask = overlay.querySelector('#reader-catalog-mask'); if (mask) mask.remove(); setIconBtnActive(overlay, 'catalog', false); } function removeSettingsImmediately(overlay) { const panel = overlay.querySelector('#reader-settings-view'); const mask = overlay.querySelector('#reader-settings-mask'); if (panel) panel.remove(); if (mask) mask.remove(); setIconBtnActive(overlay, 'settings', false); } function renderCatalogLinks(container, links, colors, onLinkClick, currentUrl, isNight, order) { container.innerHTML = ''; if (!links.length) { const errorDiv = document.createElement('div'); errorDiv.style.cssText = `padding: 30px; text-align: center; color: ${colors.subtleTextColor}; font-size: 14px;`; errorDiv.textContent = '未找到章节链接。'; container.appendChild(errorDiv); return; } const curKey = normalizeUrl(currentUrl || window.location.href); const currentBgHighlight = isNight ? 'rgba(255,255,255,0.08)' : 'rgba(0,123,255,0.08)'; const displayLinks = (order === 'desc') ? links.slice().reverse() : links; displayLinks.forEach(link => { const isMissing = link.__missing === true; const isCurrent = !isMissing && link.href && normalizeUrl(link.href) === curKey; if (isMissing) { const div = document.createElement('div'); div.dataset.missing = 'true'; div.style.cssText = ` display: flex; align-items: center; gap: 8px; padding: 12px 8px 12px 12px; color: ${colors.subtleTextColor}; font-size: 15px; border-bottom: 1px solid ${colors.borderColor}; background: transparent; line-height: 1.5; font-style: italic; opacity: 0.62; cursor: not-allowed; -webkit-tap-highlight-color: transparent; `; const iconSpan = document.createElement('span'); iconSpan.innerHTML = ICONS.missing; iconSpan.style.cssText = `flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; color: ${colors.subtleTextColor};`; div.appendChild(iconSpan); const textSpan = document.createElement('span'); textSpan.textContent = link.text; textSpan.style.cssText = 'flex: 1; overflow: hidden; text-overflow: ellipsis; text-decoration: line-through; text-decoration-thickness: 1px;'; div.appendChild(textSpan); const tag = document.createElement('span'); tag.textContent = '缺失'; tag.style.cssText = `flex-shrink: 0; font-style: normal; font-size: 11px; color: ${colors.subtleTextColor}; border: 1px solid ${colors.borderColor}; border-radius: 4px; padding: 1px 6px; opacity: 0.8;`; div.appendChild(tag); div.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); notify('此章在原站缺失,无法阅读'); }); container.appendChild(div); return; } const a = document.createElement('a'); a.href = link.href; if (isCurrent) a.dataset.current = 'true'; a.style.cssText = ` display: flex; align-items: center; gap: 8px; padding: 12px 8px 12px 12px; color: ${isCurrent ? colors.navBtnColor : colors.textColor}; text-decoration: none; font-size: 15px; font-weight: ${isCurrent ? '600' : 'normal'}; border-bottom: 1px solid ${colors.borderColor}; background: ${isCurrent ? currentBgHighlight : 'transparent'}; line-height: 1.5; transition: background 0.2s; -webkit-tap-highlight-color: transparent; `; if (isCurrent) { const cross = document.createElement('span'); cross.className = 'reader-current-crosshair'; cross.innerHTML = ICONS.crosshair; cross.style.cssText = `flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; color: ${colors.navBtnColor};`; a.appendChild(cross); } const textSpan = document.createElement('span'); textSpan.textContent = link.text; textSpan.style.cssText = 'flex: 1; overflow: hidden; text-overflow: ellipsis;'; a.appendChild(textSpan); a.addEventListener('mouseenter', () => a.style.background = colors.controlBgColor); a.addEventListener('mouseleave', () => { if (isCurrent) a.style.background = currentBgHighlight; else a.style.background = 'transparent'; }); if (onLinkClick) { a.addEventListener('click', (e) => { e.preventDefault(); onLinkClick(link.href); }); } container.appendChild(a); }); const currentEl = container.querySelector('a[data-current="true"]'); if (currentEl && order === 'asc') { setTimeout(() => { try { const listRect = container.getBoundingClientRect(); const elRect = currentEl.getBoundingClientRect(); container.scrollTop += (elRect.top - listRect.top); } catch(e) { try { currentEl.scrollIntoView({ block: 'start', behavior: 'auto' }); } catch(e2) {} } }, 50); } } function refreshCatalogColors(userBg, isNight) { if (!currentCatalogView) return; const actualBg = getEffectiveBgColor(userBg, isNight); currentCatalogView.style.backgroundColor = actualBg; const catalogColors = calculateColors(userBg, isNight); const currentBgHighlight = isNight ? 'rgba(255,255,255,0.08)' : 'rgba(0,123,255,0.08)'; currentCatalogView.querySelectorAll('a').forEach(a => { const isCurrent = a.dataset.current === 'true'; if (isCurrent) { a.style.color = catalogColors.navBtnColor; a.style.background = currentBgHighlight; a.style.borderBottomColor = catalogColors.borderColor; } else { a.style.color = catalogColors.textColor; a.style.background = 'transparent'; a.style.borderBottomColor = catalogColors.borderColor; } const cross = a.querySelector('.reader-current-crosshair'); if (cross) cross.style.color = catalogColors.navBtnColor; }); currentCatalogView.querySelectorAll('[data-missing="true"]').forEach(div => { div.style.color = catalogColors.subtleTextColor; div.style.borderBottomColor = catalogColors.borderColor; const iconSpan = div.querySelector('span'); if (iconSpan) iconSpan.style.color = catalogColors.subtleTextColor; const tag = div.querySelector('span:last-child'); if (tag) { tag.style.color = catalogColors.subtleTextColor; tag.style.borderColor = catalogColors.borderColor; } }); const locateBtn = currentCatalogView.querySelector('#reader-catalog-locate'); if (locateBtn) { locateBtn.style.background = catalogColors.controlBgColor; locateBtn.style.color = catalogColors.navBtnColor; locateBtn.style.borderColor = catalogColors.borderColor; } const sortBtn = currentCatalogView.querySelector('.reader-catalog-sort-btn'); if (sortBtn) { sortBtn.style.background = 'transparent'; sortBtn.style.color = catalogColors.subtleTextColor; } const toolbarLabel = currentCatalogView.querySelector('.reader-catalog-toolbar-label'); if (toolbarLabel) { toolbarLabel.style.color = catalogColors.subtleTextColor; } const headerSpan = currentCatalogView.querySelector('div > span'); if (headerSpan) headerSpan.style.color = catalogColors.textColor; } function showCatalog(indexUrl, overlay, onChapterNav) { const existingCatalog = overlay.querySelector('#reader-catalog-view'); if (existingCatalog) { if (typeof existingCatalog.__doClose === 'function') existingCatalog.__doClose(); return; } removeSettingsImmediately(overlay); setIconBtnActive(overlay, 'catalog', true); const isNight = overlay.__isNight === true; const userBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); const actualBg = getEffectiveBgColor(userBg, isNight); const colors = calculateColors(userBg, isNight); const immersiveBottom = overlay.querySelector('#reader-immersive-bottom'); if (immersiveBottom) { immersiveBottom.classList.add('above-drawer'); applyImmersiveBottomStyle(immersiveBottom, colors, immersiveBottom.dataset.style, true); } const mask = document.createElement('div'); mask.id = 'reader-catalog-mask'; mask.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); z-index: 1000; opacity: 0; transition: opacity 0.3s; touch-action: none;'; overlay.appendChild(mask); const catalogView = document.createElement('div'); catalogView.id = 'reader-catalog-view'; catalogView.style.cssText = `position: fixed; bottom: 0; left: 0; width: 100%; height: 78%; background-color: ${actualBg}; z-index: 1001; border-radius: 16px 16px 0 0; display: flex; flex-direction: column; transform: translateY(100%); transition: transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1); box-sizing: border-box; box-shadow: 0 -4px 12px rgba(0,0,0,0.15); overflow: hidden; overscroll-behavior: contain;`; const header = document.createElement('div'); header.style.cssText = `display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid ${colors.borderColor}; flex-shrink: 0;`; const headerTitle = document.createElement('span'); headerTitle.style.cssText = `font-size: 16px; font-weight: bold; color: ${colors.textColor};`; headerTitle.textContent = '目录'; const closeBtn = document.createElement('span'); closeBtn.textContent = '收起'; closeBtn.style.cssText = `font-size: 14px; color: ${colors.subtleTextColor}; cursor: pointer; padding: 5px; font-weight: normal; -webkit-tap-highlight-color: transparent;`; header.appendChild(headerTitle); header.appendChild(closeBtn); const doClose = () => { if (catalogView.__closing) return; catalogView.__closing = true; setIconBtnActive(overlay, 'catalog', false); if (catalogView.__pollInterval) { clearInterval(catalogView.__pollInterval); catalogView.__pollInterval = null; } catalogView.style.transform = 'translateY(100%)'; mask.style.opacity = '0'; setTimeout(() => { catalogView.remove(); mask.remove(); if (currentCatalogView === catalogView) currentCatalogView = null; if (overlay.querySelector('#reader-catalog-view') || overlay.querySelector('#reader-settings-view')) { return; } if (immersiveBottom) { immersiveBottom.classList.remove('above-drawer'); const restoreIsNight = overlay.__isNight === true; const restoreUserBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); const restoreColors = calculateColors(restoreUserBg, restoreIsNight); applyImmersiveBottomStyle( immersiveBottom, restoreColors, immersiveBottom.dataset.style, immersiveBottom.dataset.visible === 'true' ); } }, 300); }; catalogView.__doClose = doClose; closeBtn.addEventListener('click', doClose); mask.addEventListener('click', doClose); catalogView.appendChild(header); const toolbar = document.createElement('div'); toolbar.className = 'reader-catalog-toolbar'; toolbar.style.cssText = `display: flex; justify-content: space-between; align-items: center; padding: 8px 20px 6px; flex-shrink: 0; border-bottom: 1px solid ${colors.borderColor};`; const toolbarLabel = document.createElement('span'); toolbarLabel.className = 'reader-catalog-toolbar-label'; toolbarLabel.textContent = ''; toolbarLabel.style.cssText = `font-size: 12px; color: ${colors.subtleTextColor}; user-select: none;`; function updateToolbarCount(links) { if (links && links.length > 0) { let missingCount = 0; for (const l of links) if (l && l.__missing === true) missingCount++; let text = `共 ${links.length} 章`; if (missingCount > 0) { text += `(其中 ${missingCount} 章原站缺失)`; } toolbarLabel.textContent = text; } else { toolbarLabel.textContent = ''; } } const sortBtn = document.createElement('div'); sortBtn.className = 'reader-catalog-sort-btn'; sortBtn.title = '切换正序/倒序'; sortBtn.style.cssText = `display: inline-flex; align-items: center; gap: 5px; padding: 5px 8px; border-radius: 6px; font-size: 12px; font-weight: 500; line-height: 1; color: ${colors.subtleTextColor}; background: transparent; border: none; cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; transition: transform 0.12s ease;`; function updateSortBtn() { const isAsc = catalogOrder === 'asc'; const label = isAsc ? '倒序' : '正序'; const icon = isAsc ? ICONS.catalogSortDesc : ICONS.catalogSortAsc; sortBtn.innerHTML = icon + `${label}`; } updateSortBtn(); sortBtn.addEventListener('mousedown', () => { sortBtn.style.transform = 'scale(0.95)'; }); sortBtn.addEventListener('mouseup', () => { sortBtn.style.transform = ''; }); sortBtn.addEventListener('mouseleave', () => { sortBtn.style.transform = ''; }); sortBtn.addEventListener('click', (e) => { e.stopPropagation(); catalogOrder = catalogOrder === 'asc' ? 'desc' : 'asc'; updateSortBtn(); if (catalogCache && catalogCache.url === indexUrl) { try { renderCatalogLinks(listContainer, catalogCache.links, colors, handleLinkClick, overlay.__currentUrl || window.location.href, isNight, catalogOrder); listContainer.scrollTop = 0; scheduleCheck(); } catch(err) {} } }); toolbar.appendChild(toolbarLabel); toolbar.appendChild(sortBtn); catalogView.appendChild(toolbar); const listContainer = document.createElement('div'); listContainer.id = 'reader-catalog-list'; listContainer.style.cssText = 'flex: 1; overflow-y: auto; padding: 10px 20px 100px; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;'; catalogView.appendChild(listContainer); const locateBtn = document.createElement('div'); locateBtn.id = 'reader-catalog-locate'; locateBtn.className = 'reader-locate-btn'; locateBtn.title = '定位到当前章节'; locateBtn.innerHTML = ICONS.crosshair; locateBtn.style.cssText = `position: absolute; right: 16px; bottom: 80px; width: 40px; height: 40px; border-radius: 50%; background: ${colors.controlBgColor}; color: ${colors.navBtnColor}; border: 1px solid ${colors.borderColor}; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 5; box-shadow: 0 2px 10px rgba(0,0,0,0.18); -webkit-tap-highlight-color: transparent; outline: none; touch-action: none;`; locateBtn.addEventListener('click', (e) => { e.stopPropagation(); const currentEl = listContainer.querySelector('a[data-current="true"]'); if (currentEl) { try { const listRect = listContainer.getBoundingClientRect(); const elRect = currentEl.getBoundingClientRect(); listContainer.scrollTo({ top: listContainer.scrollTop + (elRect.top - listRect.top), behavior: 'smooth' }); } catch(e) { currentEl.scrollIntoView(); } } else { notify('未找到当前章节'); } }); catalogView.appendChild(locateBtn); function updateLocateBtnVisibility() { const currentEl = listContainer.querySelector('a[data-current="true"]'); if (!currentEl) { locateBtn.classList.remove('reader-locate-visible'); return; } const listRect = listContainer.getBoundingClientRect(); const elRect = currentEl.getBoundingClientRect(); const isCompletelyOut = elRect.bottom <= listRect.top || elRect.top >= listRect.bottom; locateBtn.classList.toggle('reader-locate-visible', isCompletelyOut); } catalogView.__updateLocateBtn = updateLocateBtnVisibility; listContainer.addEventListener('scroll', updateLocateBtnVisibility, { passive: true }); overlay.appendChild(catalogView); currentCatalogView = catalogView; requestAnimationFrame(() => { catalogView.style.transform = 'translateY(0)'; mask.style.opacity = '1'; }); const handleLinkClick = (url) => { doClose(); if (onChapterNav) onChapterNav(url); else window.location.href = url; }; const curUrl = overlay.__currentUrl || window.location.href; const scheduleCheck = () => { setTimeout(() => { try { updateLocateBtnVisibility(); } catch(e) {} }, 200); }; if (catalogCache && catalogCache.url === indexUrl) { updateToolbarCount(catalogCache.links); renderCatalogLinks(listContainer, catalogCache.links, colors, handleLinkClick, curUrl, isNight, catalogOrder); scheduleCheck(); return; } if (catalogCacheLoading && catalogCacheLoading.url === indexUrl) { const loadingDiv = document.createElement('div'); loadingDiv.id = 'reader-catalog-loading'; loadingDiv.style.cssText = `padding: 30px; text-align: center; color: ${colors.subtleTextColor}; font-size: 14px;`; loadingDiv.textContent = '正在加载目录...'; listContainer.appendChild(loadingDiv); const check = setInterval(() => { if (catalogCache && catalogCache.url === indexUrl) { clearInterval(check); catalogView.__pollInterval = null; const el = overlay.querySelector('#reader-catalog-loading'); if (el) el.remove(); updateToolbarCount(catalogCache.links); renderCatalogLinks(listContainer, catalogCache.links, colors, handleLinkClick, curUrl, isNight, catalogOrder); scheduleCheck(); } else if (!catalogCacheLoading) { clearInterval(check); catalogView.__pollInterval = null; const el = overlay.querySelector('#reader-catalog-loading'); if (el) el.remove(); renderError(listContainer, '目录预加载失败,请稍后重试。', indexUrl, colors); } }, 150); catalogView.__pollInterval = check; return; } const loadingDiv = document.createElement('div'); loadingDiv.id = 'reader-catalog-loading'; loadingDiv.style.cssText = `padding: 30px; text-align: center; color: ${colors.subtleTextColor}; font-size: 14px;`; loadingDiv.textContent = '正在加载目录...'; listContainer.appendChild(loadingDiv); const iframe = document.createElement('iframe'); iframe.style.cssText = 'position: absolute; width: 0; height: 0; border: none; visibility: hidden;'; iframe.src = indexUrl; document.body.appendChild(iframe); let iframeLoaded = false; const timeoutFallback = setTimeout(() => { if (!iframeLoaded) { iframe.remove(); const el = overlay.querySelector('#reader-catalog-loading'); if (el) { el.remove(); renderError(listContainer, '目录加载超时,可能是目标网站禁止嵌入(iframe)或网络问题。', indexUrl, colors); } } }, 8000); iframe.onload = function() { iframeLoaded = true; clearTimeout(timeoutFallback); const el = overlay.querySelector('#reader-catalog-loading'); if (el) el.remove(); const result = safeGetIframeDoc(iframe); if (!result.doc) { if (result.error === 'cross-origin') { renderError(listContainer, '目录页禁止跨域访问(iframe 被同源策略或 X-Frame-Options 拦截)。', indexUrl, colors); } else { renderError(listContainer, '无法访问目录页内容。', indexUrl, colors); } iframe.remove(); return; } try { const links = extractChapterLinksFromDoc(result.doc, indexUrl); if (links.length === 0) { renderError(listContainer, '未能从目录页提取到章节链接。', indexUrl, colors); return; } catalogCache = { url: indexUrl, links }; // console.log(`[阅读模式] 目录抓取成功,共 ${links.length} 条章节链接`); overlay.dispatchEvent(new CustomEvent('reader-catalog-ready')); updateToolbarCount(links); renderCatalogLinks(listContainer, links, colors, handleLinkClick, curUrl, isNight, catalogOrder); scheduleCheck(); } catch (e) { // console.error('解析 iframe 目录失败:', e); renderError(listContainer, '解析目录失败。', indexUrl, colors); } finally { iframe.remove(); } }; function renderError(container, msg, url, c) { const errorDiv = document.createElement('div'); errorDiv.style.cssText = `padding: 20px; text-align: center; color: ${c.subtleTextColor}; font-size: 14px;`; errorDiv.textContent = msg; const jumpBtn = document.createElement('button'); jumpBtn.textContent = '点此手动打开目录页'; jumpBtn.style.cssText = `margin-top: 15px; padding: 10px 20px; background: ${c.navBtnColor}; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; -webkit-tap-highlight-color: transparent; outline: none;`; jumpBtn.addEventListener('click', () => { window.open(url, '_blank'); }); errorDiv.appendChild(document.createElement('br')); errorDiv.appendChild(jumpBtn); container.appendChild(errorDiv); } } function showSettingsPanel(overlay, opts) { const existingSettings = overlay.querySelector('#reader-settings-view'); if (existingSettings) { if (typeof existingSettings.__doClose === 'function') existingSettings.__doClose(); return; } removeCatalogImmediately(overlay); setIconBtnActive(overlay, 'settings', true); const isNight = overlay.__isNight === true; const userBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); const actualBg = getEffectiveBgColor(userBg, isNight); const colors = calculateColors(userBg, isNight); const immersiveBottom = overlay.querySelector('#reader-immersive-bottom'); if (immersiveBottom) { immersiveBottom.classList.add('above-drawer'); applyImmersiveBottomStyle(immersiveBottom, colors, immersiveBottom.dataset.style, true); } const mask = document.createElement('div'); mask.id = 'reader-settings-mask'; mask.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); z-index: 1000; opacity: 0; transition: opacity 0.3s; touch-action: none;'; overlay.appendChild(mask); const panel = document.createElement('div'); panel.id = 'reader-settings-view'; panel.style.cssText = `position: fixed; bottom: 0; left: 0; width: 100%; background-color: ${actualBg}; z-index: 1001; border-radius: 16px 16px 0 0; display: flex; flex-direction: column; transform: translateY(100%); transition: transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1); box-sizing: border-box; box-shadow: 0 -4px 12px rgba(0,0,0,0.15); padding: 0 0 env(safe-area-inset-bottom, 0px); max-height: 90%; overscroll-behavior: contain; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;`; const header = document.createElement('div'); header.style.cssText = `display: flex; justify-content: space-between; align-items: center; padding: 14px 18px; border-bottom: 1px solid ${colors.borderColor}; flex-shrink: 0;`; const headerLeft = document.createElement('div'); headerLeft.style.cssText = 'display: flex; align-items: center; gap: 10px;'; const titleSpan = document.createElement('span'); titleSpan.textContent = '设置'; titleSpan.style.cssText = `font-size: 15px; font-weight: bold; color: ${colors.textColor};`; headerLeft.appendChild(titleSpan); const closeBtn = document.createElement('span'); closeBtn.textContent = '收起'; closeBtn.style.cssText = `font-size: 13px; color: ${colors.subtleTextColor}; cursor: pointer; padding: 5px; -webkit-tap-highlight-color: transparent;`; const doClose = () => { if (panel.__closing) return; panel.__closing = true; setIconBtnActive(overlay, 'settings', false); panel.style.transform = 'translateY(100%)'; mask.style.opacity = '0'; setTimeout(() => { panel.remove(); mask.remove(); if (overlay.querySelector('#reader-catalog-view') || overlay.querySelector('#reader-settings-view')) { return; } if (immersiveBottom) { immersiveBottom.classList.remove('above-drawer'); const restoreIsNight = overlay.__isNight === true; const restoreUserBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); const restoreColors = calculateColors(restoreUserBg, restoreIsNight); applyImmersiveBottomStyle( immersiveBottom, restoreColors, immersiveBottom.dataset.style, immersiveBottom.dataset.visible === 'true' ); } }, 300); }; panel.__doClose = doClose; closeBtn.addEventListener('click', doClose); mask.addEventListener('click', doClose); header.appendChild(headerLeft); header.appendChild(closeBtn); panel.appendChild(header); const body = document.createElement('div'); body.style.cssText = 'flex: 1; overflow-y: auto; padding: 16px 18px 90px; display: flex; flex-direction: column; gap: 14px; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;'; panel.appendChild(body); function renderMainView() { titleSpan.textContent = '设置'; body.innerHTML = ''; const fontSizeRow = document.createElement('div'); fontSizeRow.style.cssText = `display: flex; align-items: center; gap: 10px;`; const fontSizeLabel = document.createElement('div'); fontSizeLabel.textContent = '字号'; fontSizeLabel.style.cssText = `font-size: 13px; color: ${colors.subtleTextColor}; flex-shrink: 0; width: 40px;`; fontSizeRow.appendChild(fontSizeLabel); const sizeBtns = document.createElement('div'); sizeBtns.style.cssText = `display: flex; align-items: center; flex: 1; background: ${colors.controlBgColor}; border-radius: 8px; border: 1px solid ${colors.borderColor}; overflow: hidden;`; const btnMinus = document.createElement('div'); btnMinus.textContent = 'A-'; btnMinus.style.cssText = `flex: 1; text-align: center; padding: 8px 0; cursor: pointer; font-size: 14px; color: ${colors.textColor}; user-select: none; -webkit-tap-highlight-color: transparent;`; const sizeLabel = document.createElement('div'); sizeLabel.textContent = parseInt(gmGet(STORAGE_KEYS.FONT_SIZE, DEFAULT_SETTINGS.fontSize), 10) || 18; sizeLabel.style.cssText = `flex: 1; text-align: center; font-size: 13px; color: ${colors.textColor}; font-weight: 600; border-left: 1px solid ${colors.borderColor}; border-right: 1px solid ${colors.borderColor}; padding: 8px 0;`; const btnPlus = document.createElement('div'); btnPlus.textContent = 'A+'; btnPlus.style.cssText = btnMinus.style.cssText; btnMinus.addEventListener('mouseenter', () => btnMinus.style.background = colors.borderColor); btnMinus.addEventListener('mouseleave', () => btnMinus.style.background = 'transparent'); btnPlus.addEventListener('mouseenter', () => btnPlus.style.background = colors.borderColor); btnPlus.addEventListener('mouseleave', () => btnPlus.style.background = 'transparent'); btnMinus.addEventListener('click', () => { const cur = parseInt(gmGet(STORAGE_KEYS.FONT_SIZE, DEFAULT_SETTINGS.fontSize), 10) || 18; const next = Math.max(11, cur - 1); opts.onFontSizeChange(next + 'px'); sizeLabel.textContent = next; }); btnPlus.addEventListener('click', () => { const cur = parseInt(gmGet(STORAGE_KEYS.FONT_SIZE, DEFAULT_SETTINGS.fontSize), 10) || 18; const next = Math.min(30, cur + 1); opts.onFontSizeChange(next + 'px'); sizeLabel.textContent = next; }); sizeBtns.appendChild(btnMinus); sizeBtns.appendChild(sizeLabel); sizeBtns.appendChild(btnPlus); fontSizeRow.appendChild(sizeBtns); body.appendChild(fontSizeRow); const bgRow = document.createElement('div'); bgRow.style.cssText = `display: flex; align-items: center; gap: 10px;`; const bgLabel = document.createElement('div'); bgLabel.textContent = '背景'; bgLabel.style.cssText = `font-size: 13px; color: ${colors.subtleTextColor}; flex-shrink: 0; width: 40px;`; bgRow.appendChild(bgLabel); const swatchRow = document.createElement('div'); swatchRow.style.cssText = `display: flex; align-items: center; gap: 10px; flex: 1;`; const activeBg = gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); Object.keys(config.backgroundOptions).forEach(name => { const color = config.backgroundOptions[name]; const isSelected = color === activeBg; const swatch = document.createElement('div'); swatch.title = name; swatch.dataset.color = color; swatch.style.cssText = `width: 32px; height: 32px; border-radius: 50%; background: ${color}; cursor: pointer; position: relative; box-shadow: 0 1px 3px rgba(0,0,0,0.12); border: 2px solid ${isSelected ? colors.navBtnColor : 'transparent'}; transition: transform 0.2s; box-sizing: border-box; -webkit-tap-highlight-color: transparent;`; swatch.addEventListener('mouseenter', () => swatch.style.transform = 'scale(1.1)'); swatch.addEventListener('mouseleave', () => swatch.style.transform = 'scale(1)'); swatch.addEventListener('click', () => { opts.onBgChange(color); panel.style.background = color; swatchRow.querySelectorAll('div').forEach(el => { el.style.border = '2px solid transparent'; }); swatch.style.border = `2px solid ${colors.navBtnColor}`; }); swatchRow.appendChild(swatch); }); bgRow.appendChild(swatchRow); body.appendChild(bgRow); } renderMainView(); overlay.appendChild(panel); requestAnimationFrame(() => { panel.style.transform = 'translateY(0)'; mask.style.opacity = '1'; }); } function createNavBar(nav, colors, addHash = false, id = '', onCatalogClick, onChapterNav) { 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;-webkit-tap-highlight-color:transparent;`; if (!link) { a.addEventListener('click', e => e.preventDefault()); } else if (text === '目录' && onCatalogClick) { a.addEventListener('click', e => { e.preventDefault(); onCatalogClick(link.href); }); } else if (onChapterNav) { a.addEventListener('click', e => { e.preventDefault(); onChapterNav(finalHref); }); } return a; }; bar.appendChild(createBtn('上一页', nav.prev)); bar.appendChild(createBtn('目录', nav.index)); bar.appendChild(createBtn('下一页', nav.next)); return bar; } function applyImmersiveTopStyle(el, colors, style, visible) { if (!el) return; const transform = visible ? 'translateY(0)' : 'translateY(-100%)'; const opacity = visible ? '1' : '0'; const borderBottom = visible ? colors.borderColor : 'transparent'; const boxShadow = visible ? '0 2px 8px rgba(0,0,0,0.06)' : 'none'; el.style.cssText = `position: fixed; z-index: 90; display: flex; align-items: center; background: ${colors.controlBgColor}; transition: transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.32s cubic-bezier(0.2, 0.8, 0.2, 1), background 0.3s ease, border-color 0.3s ease, border-radius 0.3s ease, box-shadow 0.3s ease; transform: ${transform}; opacity: ${opacity}; pointer-events: ${visible ? 'auto' : 'none'}; -webkit-tap-highlight-color: transparent; outline: none; touch-action: none; overscroll-behavior: contain; color: ${colors.textColor}; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif; top: 0; left: 0; right: 0; padding: 10px 14px; border-bottom: 1px solid ${borderBottom}; border-radius: 0; box-shadow: ${boxShadow};`; } function applyImmersiveBottomStyle(el, colors, style, visible) { if (!el) return; const aboveDrawer = el.classList.contains('above-drawer'); const effectiveVisible = aboveDrawer ? true : visible; const zIndex = aboveDrawer ? 1010 : 90; const transform = effectiveVisible ? 'translateY(0)' : 'translateY(100%)'; const opacity = effectiveVisible ? '1' : '0'; const borderTop = effectiveVisible ? colors.borderColor : 'transparent'; const boxShadow = effectiveVisible ? '0 -2px 8px rgba(0,0,0,0.06)' : 'none'; el.style.cssText = `position: fixed; z-index: ${zIndex}; display: flex; flex-direction: column; background: ${colors.controlBgColor}; transition: transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.32s cubic-bezier(0.2, 0.8, 0.2, 1), background 0.3s ease, border-color 0.3s ease, border-radius 0.3s ease, box-shadow 0.3s ease; transform: ${transform}; opacity: ${opacity}; pointer-events: ${effectiveVisible ? 'auto' : 'none'}; -webkit-tap-highlight-color: transparent; outline: none; touch-action: none; overscroll-behavior: contain; color: ${colors.textColor}; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif; bottom: 0; left: 0; right: 0; padding: 0 0 env(safe-area-inset-bottom, 0px); border-top: 1px solid ${borderTop}; border-radius: 0; box-shadow: ${boxShadow};`; } function refreshImmersiveColors(container, colors, readingBg) { if (!container) return; const style = container.dataset.style || 'edge'; const visible = container.dataset.visible === 'true'; if (container.id === 'reader-immersive-top') { applyImmersiveTopStyle(container, colors, style, visible); } else { applyImmersiveBottomStyle(container, colors, style, visible); } container.querySelectorAll('.rt-btn, .rt-title').forEach(el => { el.style.color = colors.textColor; }); container.querySelectorAll('.rt-icon-btn[data-accent="true"]').forEach(el => { if (el.dataset.disabled === 'true') return; const isActive = el.dataset.active === 'true'; el.style.color = isActive ? colors.navBtnColor : colors.textColor; }); container.querySelectorAll('.rt-icon-btn[data-accent="false"]').forEach(el => { el.style.color = colors.textColor; }); container.querySelectorAll('[data-rt-color="progress-bg"]').forEach(el => { el.style.background = isDarkColor(colors.controlBgColor) ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'; }); } function setIconBtnActive(overlay, action, active) { if (!overlay) return; const btn = overlay.querySelector(`#reader-immersive-bottom .rt-icon-btn[data-action="${action}"]`); if (!btn || btn.dataset.disabled === 'true') return; btn.dataset.active = active ? 'true' : 'false'; const isNight = overlay.__isNight === true; const userBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); const colors = calculateColors(userBg, isNight); btn.style.color = active ? colors.navBtnColor : colors.textColor; } function toggleImmersiveVisibility(overlay, show) { const topEl = overlay.querySelector('#reader-immersive-top'); const botEl = overlay.querySelector('#reader-immersive-bottom'); if (!topEl || !botEl) return; topEl.dataset.visible = show ? 'true' : 'false'; botEl.dataset.visible = show ? 'true' : 'false'; const isNight = overlay.__isNight === true; const userBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); const colors = calculateColors(userBg, isNight); applyImmersiveTopStyle(topEl, colors, topEl.dataset.style, show); applyImmersiveBottomStyle(botEl, colors, botEl.dataset.style, show); } function createImmersiveTopBar(title, colors, onExit, onNewTab, style) { const bar = document.createElement('div'); bar.id = 'reader-immersive-top'; bar.dataset.style = style; bar.dataset.visible = 'false'; applyImmersiveTopStyle(bar, colors, style, false); const back = document.createElement('div'); back.className = 'rt-btn'; back.style.cssText = `display: flex; align-items: center; gap: 4px; cursor: pointer; padding: 6px 10px; border-radius: 6px; flex-shrink: 0; color: ${colors.textColor}; -webkit-tap-highlight-color: transparent; outline: none;`; back.innerHTML = ICONS.back + `返回`; back.addEventListener('click', onExit); const titleEl = document.createElement('div'); titleEl.className = 'rt-title'; titleEl.textContent = title || ''; titleEl.style.cssText = `flex: 1; text-align: center; font-size: 14px; font-weight: 500; color: ${colors.textColor}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0 8px; transition: color 0.3s ease;`; const openBtn = document.createElement('div'); openBtn.className = 'rt-btn'; openBtn.style.cssText = `display: flex; align-items: center; gap: 4px; cursor: pointer; padding: 6px 10px; border-radius: 6px; flex-shrink: 0; color: ${colors.textColor}; -webkit-tap-highlight-color: transparent; outline: none;`; openBtn.innerHTML = `打开` + ICONS.open; openBtn.addEventListener('click', onNewTab); bar.appendChild(back); bar.appendChild(titleEl); bar.appendChild(openBtn); return bar; } function createImmersiveBottomBar(colors, onCatalogClick, onSettingsClick, onNightModeClick, getThemeMode, nav, style, getCurrentNav, onChapterNav, getAccentForClick) { const bar = document.createElement('div'); bar.id = 'reader-immersive-bottom'; bar.dataset.style = style; bar.dataset.visible = 'false'; applyImmersiveBottomStyle(bar, colors, style, false); const row1 = document.createElement('div'); row1.className = 'rib-row1'; const prevBtn = document.createElement('div'); prevBtn.className = 'rt-btn'; prevBtn.id = 'reader-prev-btn'; prevBtn.textContent = '上一章'; prevBtn.style.cssText = `font-size: 13px; color: ${colors.textColor}; cursor: ${nav && nav.prev ? 'pointer' : 'not-allowed'}; opacity: ${nav && nav.prev ? 1 : 0.4}; padding: 4px 8px; transition: color 0.3s ease; -webkit-tap-highlight-color: transparent;`; prevBtn.addEventListener('click', () => { const currentNav = getCurrentNav ? getCurrentNav() : nav; if (currentNav && currentNav.prev) { if (onChapterNav) onChapterNav(currentNav.prev.href); else window.location.href = currentNav.prev.href; } }); const progressContainer = document.createElement('div'); progressContainer.setAttribute('data-rt-color', 'progress-bg'); progressContainer.style.cssText = `flex: 1; height: 4px; background: ${isDarkColor(colors.controlBgColor) ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}; border-radius: 2px; margin: 0 12px; position: relative; cursor: default; opacity: 0.5; touch-action: none;`; const progressThumb = document.createElement('div'); progressThumb.id = 'reader-progress-thumb'; progressThumb.style.cssText = `position: absolute; left: calc(0% - 7px); top: 50%; transform: translateY(-50%); width: 14px; height: 14px; background: ${colors.navBtnColor}; border-radius: 50%; box-shadow: 0 1px 4px rgba(0,0,0,0.3); transition: left 0.3s ease;`; progressContainer.appendChild(progressThumb); let isDraggingProgress = false; const canDragProgress = () => catalogCache && catalogCache.links && catalogCache.links.length > 1; const updateThumbByX = (clientX) => { const rect = progressContainer.getBoundingClientRect(); if (!rect.width) return 0; let percent = (clientX - rect.left) / rect.width; percent = Math.max(0, Math.min(1, percent)); progressThumb.style.transition = 'none'; progressThumb.style.left = `calc(${percent * 100}% - 7px)`; return percent; }; const finishProgressDrag = (clientX) => { progressThumb.style.transition = 'left 0.3s ease'; if (!canDragProgress()) return; const percent = updateThumbByX(clientX); const idx = Math.round(percent * (catalogCache.links.length - 1)); const target = catalogCache.links[idx]; if (!target || target.__missing || !target.href) { if (target && target.__missing) notify('此章在原站缺失'); return; } if (onChapterNav) onChapterNav(target.href); else window.location.href = target.href; }; const onProgressTouchStart = (e) => { if (!canDragProgress()) return; if (!e.touches || !e.touches.length) return; isDraggingProgress = true; updateThumbByX(e.touches[0].clientX); e.stopPropagation(); }; const onProgressTouchMove = (e) => { if (!isDraggingProgress) return; if (!e.touches || !e.touches.length) return; updateThumbByX(e.touches[0].clientX); e.stopPropagation(); }; const onProgressTouchEnd = (e) => { if (!isDraggingProgress) return; isDraggingProgress = false; const t = (e.changedTouches && e.changedTouches[0]) ? e.changedTouches[0].clientX : null; if (t !== null) finishProgressDrag(t); }; const onProgressMouseDown = (e) => { if (!canDragProgress()) return; isDraggingProgress = true; updateThumbByX(e.clientX); e.stopPropagation(); }; const onProgressMouseMove = (e) => { if (!isDraggingProgress) return; updateThumbByX(e.clientX); }; const onProgressMouseUp = (e) => { if (!isDraggingProgress) return; isDraggingProgress = false; finishProgressDrag(e.clientX); }; progressContainer.addEventListener('touchstart', onProgressTouchStart, { passive: true }); progressContainer.addEventListener('touchmove', onProgressTouchMove, { passive: true }); progressContainer.addEventListener('touchend', onProgressTouchEnd); progressContainer.addEventListener('mousedown', onProgressMouseDown); window.addEventListener('mousemove', onProgressMouseMove); window.addEventListener('mouseup', onProgressMouseUp); bar.__cleanup = () => { window.removeEventListener('mousemove', onProgressMouseMove); window.removeEventListener('mouseup', onProgressMouseUp); }; const nextBtn = document.createElement('div'); nextBtn.className = 'rt-btn'; nextBtn.id = 'reader-next-btn'; nextBtn.textContent = '下一章'; nextBtn.style.cssText = `font-size: 13px; color: ${colors.textColor}; cursor: ${nav && nav.next ? 'pointer' : 'not-allowed'}; opacity: ${nav && nav.next ? 1 : 0.4}; padding: 4px 8px; transition: color 0.3s ease; -webkit-tap-highlight-color: transparent;`; nextBtn.addEventListener('click', () => { const currentNav = getCurrentNav ? getCurrentNav() : nav; if (currentNav && currentNav.next) { if (onChapterNav) onChapterNav(currentNav.next.href); else window.location.href = currentNav.next.href; } }); row1.appendChild(prevBtn); row1.appendChild(progressContainer); row1.appendChild(nextBtn); bar.appendChild(row1); const row2 = document.createElement('div'); row2.style.cssText = 'display: flex; align-items: center; justify-content: space-around; padding: 4px 16px 10px;'; function makeIconBtn(iconSvg, label, onClick, disabled, action) { const btn = document.createElement('div'); btn.className = 'rt-icon-btn'; btn.dataset.accent = 'true'; btn.dataset.action = action || ''; if (disabled) btn.dataset.disabled = 'true'; const baseColor = disabled ? colors.subtleTextColor : colors.textColor; btn.style.cssText = `display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; padding: 6px 12px; cursor: ${disabled ? 'not-allowed' : 'pointer'}; user-select: none; border-radius: 8px; transition: color 0.25s ease, transform 0.15s ease, opacity 0.2s ease; opacity: ${disabled ? 0.4 : 1}; color: ${baseColor}; -webkit-tap-highlight-color: transparent; outline: none;`; btn.innerHTML = iconSvg + `${label}`; if (!disabled) { btn.addEventListener('click', () => { const accent = getAccentForClick(); btn.style.color = accent; btn.style.transform = 'scale(0.92)'; setTimeout(() => { btn.style.transform = ''; const restoreColors = bar.__getRestoreColors ? bar.__getRestoreColors() : colors; btn.style.color = (btn.dataset.active === 'true') ? restoreColors.navBtnColor : restoreColors.textColor; }, 150); onClick(); }); } return btn; } row2.appendChild(makeIconBtn( ICONS.catalog, '目录', () => { const currentNav = getCurrentNav ? getCurrentNav() : nav; if (currentNav && currentNav.index) onCatalogClick(currentNav.index.href); }, !nav || !nav.index, 'catalog' )); const themeBtn = document.createElement('div'); themeBtn.className = 'rt-icon-btn'; themeBtn.dataset.accent = 'false'; themeBtn.style.cssText = `display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; padding: 6px 12px; cursor: pointer; user-select: none; border-radius: 8px; transition: color 0.25s ease; color: ${colors.textColor}; -webkit-tap-highlight-color: transparent; outline: none;`; function updateThemeBtn() { const mode = getThemeMode(); let icon, label; if (mode === 'night') { icon = ICONS.sun; label = '日间'; } else { icon = ICONS.moon; label = '夜间'; } themeBtn.innerHTML = icon + `${label}`; } updateThemeBtn(); themeBtn.addEventListener('click', () => { onNightModeClick(); updateThemeBtn(); }); row2.appendChild(themeBtn); row2.appendChild(makeIconBtn(ICONS.settings, '设置', onSettingsClick, false, 'settings')); bar.appendChild(row2); bar.__updateThemeBtn = updateThemeBtn; return bar; } function refreshImmersiveNavButtons(overlay, nav) { const prevBtn = overlay.querySelector('#reader-prev-btn'); const nextBtn = overlay.querySelector('#reader-next-btn'); if (prevBtn) { prevBtn.style.cursor = nav && nav.prev ? 'pointer' : 'not-allowed'; prevBtn.style.opacity = nav && nav.prev ? '1' : '0.4'; } if (nextBtn) { nextBtn.style.cursor = nav && nav.next ? 'pointer' : 'not-allowed'; nextBtn.style.opacity = nav && nav.next ? '1' : '0.4'; } } function loadChapterInPlace(url, overlay) { if (!url || !overlay) return; const isNewTabMode = overlay.__addHashToNav === true; const navUrl = isNewTabMode ? addReaderModeHash(url) : url; const goFallback = () => { notify('章节加载失败,跳转到新页面'); setTimeout(() => { window.location.href = navUrl; }, 800); }; const cacheKey = normalizeUrl(url); const cached = chapterCache.get(cacheKey); if (cached && cached.article) { chapterCache.delete(cacheKey); try { updateReaderContentFull(overlay, cached.article); overlay.__currentUrl = url; try { history.pushState({}, '', navUrl); } catch(e) {} updateProgressBar(overlay, url); schedulePreloadNext(overlay); return; } catch(e) { // console.error('[阅读模式] 缓存渲染异常,回退到加载流程:', e); } } if (currentPreloadTask && normalizeUrl(currentPreloadTask.url) === cacheKey) { cancelCurrentPreload(); } const loadingMask = document.createElement('div'); loadingMask.id = 'reader-chapter-loading'; loadingMask.style.cssText = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.3); z-index: 2147483000; display: flex; align-items: center; justify-content: center; pointer-events: auto; touch-action: none;`; const loadingBox = document.createElement('div'); loadingBox.textContent = '正在加载...'; loadingBox.style.cssText = `background: rgba(0,0,0,0.75); color: #fff; padding: 12px 24px; border-radius: 8px; font-size: 14px;`; loadingMask.appendChild(loadingBox); overlay.appendChild(loadingMask); const iframe = document.createElement('iframe'); iframe.style.cssText = 'position:absolute;width:0;height:0;border:none;visibility:hidden;'; iframe.src = url; document.body.appendChild(iframe); let loaded = false; const timeout = setTimeout(() => { if (!loaded) { iframe.remove(); loadingMask.remove(); goFallback(); } }, 15000); iframe.onload = function() { loaded = true; clearTimeout(timeout); const result = safeGetIframeDoc(iframe); if (!result.doc) { iframe.remove(); loadingMask.remove(); goFallback(); return; } let pollCount = 0; const MAX_POLL_COUNT = 15; const POLL_INTERVAL = 200; const finishFail = (reason) => { // console.error('[阅读模式] 动态加载失败:', reason || '正文轮询超时'); iframe.remove(); loadingMask.remove(); goFallback(); }; const tryExtract = () => { let article = null; try { article = extractContent(result.doc); } catch (e) { // console.error('[阅读模式] 章节提取异常:', e); } if (article) { try { updateReaderContentFull(overlay, article); overlay.__currentUrl = url; try { history.pushState({}, '', navUrl); } catch(e) {} updateProgressBar(overlay, url); schedulePreloadNext(overlay); } catch (e) { finishFail('章节渲染异常: ' + e.message); return; } iframe.remove(); loadingMask.remove(); return; } pollCount++; if (pollCount < MAX_POLL_COUNT) { setTimeout(tryExtract, POLL_INTERVAL); } else { finishFail('正文轮询超时'); } }; tryExtract(); }; } function updateReaderContentFull(overlay, article) { if (!overlay || !article) return; const titleEl = overlay.querySelector('#reader-article-title'); const contentSec = overlay.querySelector('#reader-content-section'); const bottomNav = overlay.querySelector('#reader-bottom-nav'); const styleEl = overlay.querySelector('#reader-content-style'); const immersiveTitle = overlay.querySelector('#reader-immersive-top .rt-title'); overlay.__nav = article.nav; const isNight = overlay.__isNight === true; const userBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); const colors = calculateColors(userBg, isNight); if (titleEl) { titleEl.textContent = article.title || ''; titleEl.style.color = colors.textColor; titleEl.style.textAlign = 'left'; } if (immersiveTitle) { immersiveTitle.textContent = article.title || ''; } if (contentSec) { contentSec.innerHTML = article.content; contentSec.style.color = colors.textColor; } if (styleEl) { styleEl.textContent = buildContentStyleCSS(userBg, article.autoHasIndent, isNight); } const addHashToNav = overlay.__addHashToNav || false; const onChapterNav = overlay.__onChapterNav || ((u) => { window.location.href = u; }); const onCatalogClick = (u) => showCatalog(u, overlay, onChapterNav); const newBottom = createNavBar(article.nav, colors, addHashToNav, 'reader-bottom-nav', onCatalogClick, onChapterNav); if (bottomNav && newBottom) bottomNav.replaceWith(newBottom); else if (bottomNav && !newBottom) bottomNav.remove(); else if (!bottomNav && newBottom && contentSec && contentSec.parentNode) { contentSec.parentNode.appendChild(newBottom); } refreshImmersiveNavButtons(overlay, article.nav); overlay.scrollTo(0, 0); } function createShadowHost(hostId, bgColor) { const host = document.createElement('div'); host.id = hostId; host.style.cssText = 'all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none;'; if (bgColor) host.style.background = bgColor; const shadow = host.attachShadow({ mode: 'open' }); document.documentElement.appendChild(host); return { host, shadow }; } function buildReaderCore(article, initialUserBg, initialFontSize, onExit, overlayId, addHashToNav) { let isNight = normalizeThemeMode(gmGet(STORAGE_KEYS.THEME_MODE, DEFAULT_SETTINGS.themeMode)) === 'night'; const initialActualBg = getEffectiveBgColor(initialUserBg, isNight); const initialColors = calculateColors(initialUserBg, isNight); const { host, shadow } = createShadowHost(overlayId + '-host', initialActualBg); const baseStyle = document.createElement('style'); baseStyle.textContent = ` .rib-row1 { display: flex; align-items: center; justify-content: space-between; padding: 8px 16px; } #reader-immersive-bottom.above-drawer .rib-row1 { display: none; } .reader-locate-btn { opacity: 0; transform: scale(0.85) translateY(6px); pointer-events: none; transition: opacity 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.25s ease, background 0.25s ease, color 0.25s ease, border-color 0.25s ease; will-change: opacity, transform; } .reader-locate-btn.reader-locate-visible { opacity: 1; transform: scale(1) translateY(0); pointer-events: auto; } .reader-locate-btn.reader-locate-visible:hover { transform: scale(1.08) translateY(0); box-shadow: 0 4px 14px rgba(0,0,0,0.24); } .reader-locate-btn.reader-locate-visible:active { transform: scale(0.97) translateY(0); box-shadow: 0 2px 8px rgba(0,0,0,0.2); } `; shadow.appendChild(baseStyle); ['click', 'mousedown', 'mouseup', 'pointerdown', 'pointerup', 'touchstart', 'touchend', 'contextmenu', 'dblclick', 'auxclick'].forEach(evt => { host.addEventListener(evt, (e) => { e.stopPropagation(); }, false); }); const overlay = document.createElement('div'); overlay.id = overlayId; overlay.__shadowHost = host; overlay.__isNight = isNight; overlay.__userBgColor = initialUserBg; overlay.style.cssText = `position:absolute;top:0;left:0;width:100%;height:100%;background-color:${initialActualBg};overflow-y:auto;pointer-events:auto;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;-webkit-tap-highlight-color:transparent;outline:none;`; const mainContainer = document.createElement('div'); mainContainer.id = 'reader-container'; mainContainer.style.cssText = `width:92%;max-width:800px;margin:0 auto;padding:20px 10px;color:${initialColors.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; let bottomNavBar = null; const style = document.createElement('style'); style.id = 'reader-content-style'; style.textContent = buildContentStyleCSS(initialUserBg, article.autoHasIndent, isNight); mainContainer.appendChild(style); overlay.__addHashToNav = addHashToNav; overlay.__nav = article.nav; overlay.__currentUrl = window.location.href; const onChapterNav = (url) => { loadChapterInPlace(url, overlay); }; overlay.__onChapterNav = onChapterNav; const onCatalogClick = (url) => { showCatalog(url, overlay, onChapterNav); }; const applyReaderColors = () => { const nowIsNight = overlay.__isNight === true; const nowUserBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); const nowActualBg = getEffectiveBgColor(nowUserBg, nowIsNight); const colors = calculateColors(nowUserBg, nowIsNight); overlay.style.backgroundColor = nowActualBg; if (overlay.__shadowHost) overlay.__shadowHost.style.background = nowActualBg; mainContainer.style.color = colors.textColor; if (contentSection) contentSection.style.color = colors.textColor; if (articleTitle) articleTitle.style.color = colors.textColor; style.textContent = buildContentStyleCSS(nowUserBg, article.autoHasIndent, nowIsNight); refreshImmersiveColors(overlay.querySelector('#reader-immersive-top'), colors, nowActualBg); refreshImmersiveColors(overlay.querySelector('#reader-immersive-bottom'), colors, nowActualBg); refreshCatalogColors(nowUserBg, nowIsNight); }; const handleBgChange = (newColor) => { if (overlay.__isNight) { overlay.__isNight = false; gmSet(STORAGE_KEYS.THEME_MODE, 'day'); const bottomBar = overlay.querySelector('#reader-immersive-bottom'); if (bottomBar && typeof bottomBar.__updateThemeBtn === 'function') bottomBar.__updateThemeBtn(); } overlay.__userBgColor = newColor; saveSetting(STORAGE_KEYS.BG_COLOR, newColor); applyReaderColors(); }; const handleFontSizeChange = (newSize) => { mainContainer.style.fontSize = newSize; if (contentSection) contentSection.style.fontSize = newSize; saveSetting(STORAGE_KEYS.FONT_SIZE, newSize); }; const handleNightModeClick = () => { overlay.__isNight = !overlay.__isNight; gmSet(STORAGE_KEYS.THEME_MODE, overlay.__isNight ? 'night' : 'day'); applyReaderColors(); }; const getThemeMode = () => overlay.__isNight ? 'night' : 'day'; const getAccentForClick = () => { const nowIsNight = overlay.__isNight === true; const nowUserBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); return getAccentColor(nowUserBg, nowIsNight); }; const getRestoreColors = () => { const nowIsNight = overlay.__isNight === true; const nowUserBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); return calculateColors(nowUserBg, nowIsNight); }; 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:${initialColors.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:${initialColors.textColor};user-select:text;overflow-wrap:break-word;word-wrap:break-word;max-width:100%;text-align:left;`; bottomNavBar = createNavBar(article.nav, initialColors, addHashToNav, 'reader-bottom-nav', onCatalogClick, onChapterNav); if (articleTitle) contentArea.appendChild(articleTitle); contentArea.appendChild(contentSection); if (bottomNavBar) contentArea.appendChild(bottomNavBar); mainContainer.appendChild(contentArea); overlay.appendChild(mainContainer); const immersiveTop = createImmersiveTopBar(article.title, initialColors, onExit, () => { window.open(addReaderModeHash(originalUrl || window.location.href), '_blank'); }, 'edge'); const immersiveBottom = createImmersiveBottomBar( initialColors, onCatalogClick, () => showSettingsPanel(overlay, { onBgChange: handleBgChange, onFontSizeChange: handleFontSizeChange }), handleNightModeClick, getThemeMode, article.nav, 'edge', () => overlay.__nav, onChapterNav, getAccentForClick ); immersiveBottom.__getRestoreColors = getRestoreColors; overlay.appendChild(immersiveTop); overlay.appendChild(immersiveBottom); overlay.addEventListener('reader-catalog-ready', () => { updateProgressBar(overlay); }); updateProgressBar(overlay); let immersiveVisible = false; overlay.addEventListener('click', (e) => { const t = e.target; if (t && typeof t.closest === 'function' && t.closest(INTERACTIVE_SELECTOR)) return; if (t.closest('#reader-immersive-top') || t.closest('#reader-immersive-bottom') || t.closest('#reader-top-nav') || t.closest('#reader-catalog-view') || t.closest('#reader-catalog-mask') || t.closest('#reader-settings-view') || t.closest('#reader-settings-mask') || t.closest('#reader-chapter-loading')) return; immersiveVisible = !immersiveVisible; toggleImmersiveVisibility(overlay, immersiveVisible); }); overlay.addEventListener('scroll', () => { if (immersiveVisible) { immersiveVisible = false; toggleImmersiveVisibility(overlay, false); } }, { passive: true }); shadow.appendChild(overlay); overlay.focus(); if (article.nav && article.nav.index) { setTimeout(() => { preloadCatalog(article.nav.index.href, overlay); }, 200); } if (article.nav && article.nav.next) { schedulePreloadNext(overlay); } 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); let overlay; try { overlay = buildReaderCore(article, initialBgColor, initialFontSize, () => window.close(), 'reader-newtab-overlay', true); } catch(e) { // console.error('[阅读模式] 新标签页构建失败,恢复原页面:', e); while (hiddenContainer.firstChild) document.body.appendChild(hiddenContainer.firstChild); hiddenContainer.remove(); throw e; } 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;-webkit-tap-highlight-color:transparent;outline: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) { notify('未找到可阅读的正文内容'); 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(); readerOverlay = buildOverlayReaderUI(article, settings.bgColor, settings.fontSize); isReaderMode = true; updateMenuCommands(); }, 50); } function exitLegacyReaderMode() { if (!isReaderMode || !readerOverlay) return; const immersiveBottom = readerOverlay.querySelector('#reader-immersive-bottom'); if (immersiveBottom && typeof immersiveBottom.__cleanup === 'function') { immersiveBottom.__cleanup(); } if (preloadScheduleTimer) { clearTimeout(preloadScheduleTimer); preloadScheduleTimer = null; } cancelCurrentPreload(); chapterCache.clear(); const target = readerOverlay.__shadowHost || readerOverlay; target.remove(); readerOverlay = null; currentCatalogView = null; catalogCache = null; catalogCacheLoading = 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(); try { buildNewTabReaderUI(article, settings.bgColor, settings.fontSize); } catch(e) { notify('阅读模式初始化失败'); return; } updateMenuCommands(); } else { attempts++; if (attempts < maxAttempts) setTimeout(tryExtract, 300); else if (isMenuTriggered) { notify('未找到可阅读的正文内容,可能加载超时'); 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(); })();