// ==UserScript== // @name 阅读模式·纯净版 // @namespace https://viayoo.com/hqzw7k // @version 22.2.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', '#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', '#acontent', '#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' ]; const TITLE_SELECTORS = [ 'h1.article-title', 'h1.entry-title', 'h1.post-title', 'h1.title', 'h1#title', 'h1#articleTitle', 'h1[itemprop="headline"]', '#nr_title', '.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 PAGE_CLEAN_LINK_TEXTS = [ '举报', '回复', '首页', '返回', '立即阅读', '开始阅读', '全文阅读', '加入书架', '推荐本书', '下载TXT', '文本下载', '『加入书签,方便阅读』' ]; const PAGE_CLEAN_AD_KEYWORDS = [ '收藏备用网址', '备用网址', '收藏本站', '收藏此页', '防走丢', '谨防走丢', '请记住本站', '记住本站', '永久网址', '最新网址', '发布页', '防丢失' ]; const AD_BLOCK_TAGS = 'p, article, section, ul, ol, table, pre, blockquote, h1, h2, h3, h4, h5, h6'; const INTERACTIVE_SELECTOR = 'a, img, button, summary, details, input, label, select, textarea, video, audio, [onclick], [role="button"], [contenteditable="true"]'; const SHOULD_AUTO_ACTIVATE = window.location.hash === '#readermode'; const ICONS = { back: ``, catalog: ``, settings: ``, moon: ``, sun: ``, crosshair: ``, prev: ``, next: ``, bg: ``, open: ``, translate: ``, catalogSortAsc: ``, catalogSortDesc: ``, missing: `` }; const STORAGE_KEYS = { BG_COLOR: 'reader_bg_color', FONT_SIZE: 'reader_font_size', THEME_MODE: 'reader_theme_mode', FLOATING_BUTTON: 'reader_floating_button', }; const DEFAULT_SETTINGS = { bgColor: '#ffffff', fontSize: '18px', themeMode: 'day', floatingButton: true, }; const MIN_CONTENT_LENGTH = 30; 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)), floatingButton: gmGet(STORAGE_KEYS.FLOATING_BUTTON, DEFAULT_SETTINGS.floatingButton) === true, }; } function normalizeThemeMode(mode) { return mode === 'night' ? 'night' : 'day'; } function saveSetting(key, value) { gmSet(key, value); } function createSandboxIframe(src) { const iframe = document.createElement('iframe'); iframe.style.cssText = 'position:absolute;width:0;height:0;border:none;visibility:hidden;'; iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin'); iframe.src = src; return iframe; } let toastHost = null, toastShadow = null, 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 originalBodyOverflow = '', originalBodyPosition = '', originalBodyTop = '', 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', '米黄': '#f8f1e3', '豆沙绿': '#c0edc7', '藕粉': '#f5e6e8', '深灰': '#4a4a4d', '护眼黑': '#121212' } }; 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 cleanPageAds(root) { root = root || document.body; if (!root || !root.querySelectorAll) return; if (PAGE_CLEAN_LINK_TEXTS.length) { const esc = t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const exactSet = new Set(PAGE_CLEAN_LINK_TEXTS); const suffixRe = new RegExp('^(' + PAGE_CLEAN_LINK_TEXTS.map(esc).join('|') + ')[((]\\d*[))]?$'); root.querySelectorAll('a').forEach(a => { const t = (a.textContent || '').trim(); if (exactSet.has(t) || suffixRe.test(t)) { try { a.remove(); } catch(e) {} } }); } root.querySelectorAll('div, span, p, section, aside').forEach(el => { if (!el.isConnected) return; if (el.querySelector(AD_BLOCK_TAGS)) return; const t = (el.textContent || '').trim(); if (!t || t.length > 120) return; if (!PAGE_CLEAN_AD_KEYWORDS.some(k => t.includes(k))) return; let target = el; const p = el.parentElement; if (p && p !== root && p.children.length === 1 && !p.querySelector(AD_BLOCK_TAGS)) target = p; try { target.remove(); } catch(e) {} }); } function pruneCandidates(root) { if (!root || !root.querySelectorAll) return; const junkSel = [ 'aside', 'nav', 'footer', 'header', '[class*="comment"]', '[class*="sidebar"]', '[class*="footer"]', '[class*="banner"]', '[class*="promo"]', '[class*="related"]', '[class*="widget"]', '[class*="share"]', '[id*="comment"]', '[id*="sidebar"]' ].join(','); root.querySelectorAll(junkSel).forEach(el => { try { el.remove(); } catch(e) {} }); } 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']); Array.from(root.querySelectorAll('div')).forEach(block => { 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 = [], 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) => 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']); Array.from(root.querySelectorAll('div')).forEach(div => { try { const children = Array.from(div.childNodes); if (children.some(n => n.nodeType === 1 && n.tagName === 'P')) return; const hasBlockChild = children.some(n => n.nodeType === 1 && BLOCK_TAGS.has(n.tagName)); if (!hasBlockChild) return; const hasLooseText = children.some(n => n.nodeType === 3 && (n.nodeValue || '').trim().length > 0); if (!hasLooseText) return; const newChildren = []; let buffer = []; const flush = () => { if (buffer.length === 0) return; const hasContent = buffer.some(n => n.nodeType === 3 ? (n.nodeValue || '').trim().length > 0 : 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 unwrapFakeLists(root) { if (!root || !root.querySelectorAll) return; const isFakeList = (list) => { const items = Array.from(list.children).filter(c => c.tagName === 'LI'); if (items.length === 0) return false; const longItem = items.some(li => (li.textContent || '').replace(/\s+/g, '').length > 300); const manyBr = items.length > 0 && items.every(li => li.querySelectorAll('br').length >= 3); return items.length === 1 || longItem || manyBr; }; const buildReplacement = (list) => { const items = Array.from(list.children).filter(c => c.tagName === 'LI'); const div = document.createElement('div'); for (const li of items) { const innerDiv = document.createElement('div'); while (li.firstChild) innerDiv.appendChild(li.firstChild); div.appendChild(innerDiv); } return div; }; root.querySelectorAll('ul, ol').forEach(list => { if (!isFakeList(list)) return; try { list.replaceWith(buildReplacement(list)); } catch(e) {} }); if (root.tagName === 'UL' || root.tagName === 'OL') { if (isFakeList(root)) { const div = buildReplacement(root); while (root.firstChild) root.removeChild(root.firstChild); while (div.firstChild) root.appendChild(div.firstChild); } } } 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); } }); cleanPageAds(c); 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; if ((table.textContent || '').replace(/\s+/g, '') !== (pre.textContent || '').replace(/\s+/g, '')) 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', 'font'].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(); }); unwrapFakeLists(c); if (shouldSplit) splitBrParagraphs(c); wrapLooseTextNodes(c); stripLeadingSpaces(c); } function removeDuplicateTitle(c, titleText) { if (!titleText) return; const norm = s => (s || '').replace(/\s+/g, '').trim(); const target = norm(titleText); if (target.length < 2) return; c.querySelectorAll('h1, h2, h3, h4, h5, h6, p, div').forEach(el => { if (el.querySelector('p, div, h1, h2, h3, h4, h5, h6, ul, ol, table')) return; if (norm(el.textContent) === target) el.remove(); }); } function detectIndent(el) { const blocks = el.querySelectorAll('p, li, div'); if (blocks.length > 0 && el.ownerDocument && el.ownerDocument.defaultView) { 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 ownerDoc = el.ownerDocument || document; const walker = ownerDoc.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 collectHiddenPaths(root) { const paths = []; if (!root || !root.childNodes) return paths; const doc = root.ownerDocument || document; const win = doc && doc.defaultView; if (!win || typeof win.getComputedStyle !== 'function') return paths; (function walk(el, path) { let hidden = false; try { const cs = win.getComputedStyle(el); if (cs.display === 'none' || cs.visibility === 'hidden') hidden = true; } catch(e) {} if (hidden) { paths.push(path); return; } const children = el.childNodes; for (let i = 0; i < children.length; i++) { if (children[i].nodeType === 1) { walk(children[i], path.concat([i])); } } })(root, []); return paths; } function removeByPaths(clone, paths) { if (!clone || !paths || !paths.length) return; const sorted = paths.slice().sort((a, b) => b.length - a.length); for (const path of sorted) { let cur = clone; let ok = true; for (let i = 0; i < path.length - 1; i++) { cur = cur.childNodes[path[i]]; if (!cur) { ok = false; break; } } if (!ok) continue; const lastIdx = path[path.length - 1]; const target = cur.childNodes[lastIdx]; if (target && target.parentNode === cur) { try { cur.removeChild(target); } catch(e) {} } } } const SCORING_POSITIVE_RE = /(^|[-_])(content|article|post|read|chapter|text|entry|main|story|blog|body|detail|novel|txt|book|paper|doc|markdown)([-_]|$)/i; const SCORING_NEGATIVE_RE = /(^|[-_])(comment|sidebar|nav|footer|header|menu|ad|banner|promo|share|social|related|recommend|widget|breadcrumb|pagination|meta|toolbar|popup|modal|login|register|subscribe|signup|download|app)([-_]|$)/i; function findContentByScoring(doc) { const root = doc.body || doc.documentElement; if (!root) return null; const candidates = root.querySelectorAll('p, div, article, section, td, pre, blockquote, main'); const scores = new Map(); const linkDensityCache = new Map(); const JUNK_TAG_SET = new Set(['ASIDE', 'NAV', 'FOOTER', 'HEADER']); const JUNK_RE = /(^|[-_])(comment|sidebar|footer|banner|promo|related|widget|share|ad|nav)([-_]|$)/i; function isJunkNode(el) { if (JUNK_TAG_SET.has(el.tagName)) return true; const cn = ((el.className || '') + ' ' + (el.id || '')).trim(); return cn ? JUNK_RE.test(cn) : false; } function getLinkDensity(el) { if (linkDensityCache.has(el)) return linkDensityCache.get(el); const totalText = (el.textContent || '').replace(/\s+/g, '').length; if (totalText === 0) { linkDensityCache.set(el, 0); return 0; } let linkText = 0; el.querySelectorAll('a').forEach(a => { linkText += (a.textContent || '').replace(/\s+/g, '').length; }); const d = linkText / totalText; linkDensityCache.set(el, d); return d; } function effectiveTextLen(el) { let total = 0; let longPCount = 0; el.querySelectorAll('p').forEach(p => { const len = (p.textContent || '').replace(/\s+/g, '').length; if (len >= 25) { total += len; longPCount++; } }); if (longPCount === 0) { return (el.textContent || '').replace(/\s+/g, '').length; } return total; } candidates.forEach(el => { const tag = el.tagName; if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT') return; if (isJunkNode(el)) return; const rawText = (el.textContent || '').trim(); if (!rawText) return; const textLen = effectiveTextLen(el); if (textLen < 30) return; let score = 0; if (tag === 'ARTICLE') score += 30; else if (tag === 'MAIN') score += 20; else if (tag === 'SECTION') score += 15; else if (tag === 'DIV') score += 5; else if (tag === 'P') score += 3; else if (tag === 'PRE' || tag === 'BLOCKQUOTE') score += 3; else if (tag === 'OL' || tag === 'UL' || tag === 'LI' || tag === 'FORM' || tag === 'ADDRESS') score -= 3; else if (tag === 'H1' || tag === 'H2' || tag === 'H3' || tag === 'H4' || tag === 'H5' || tag === 'H6' || tag === 'TH') score -= 5; score += Math.min(textLen / 25, 30); const punctCount = (rawText.match(/[,。!?;:,.!?;:]/g) || []).length; score += Math.min(punctCount, 10); const pCount = el.querySelectorAll('p').length; if (pCount >= 3) score += Math.min(pCount, 10); const cn = ((el.className || '') + ' ' + (el.id || '')).trim(); if (cn) { if (SCORING_POSITIVE_RE.test(cn)) score += 25; if (SCORING_NEGATIVE_RE.test(cn)) score -= 25; } const ld = getLinkDensity(el); if (ld >= 0.5) return; if (ld > 0.3) score *= (1 - ld); scores.set(el, score); }); if (scores.size === 0) return null; const totals = new Map(); scores.forEach((s, el) => { let cur = el, weight = 1, depth = 0; while (cur && depth < 4) { totals.set(cur, (totals.get(cur) || 0) + s * weight); cur = cur.parentElement; weight *= 0.5; depth++; } }); let best = null, bestScore = 0; totals.forEach((s, el) => { const textLen = (el.textContent || '').replace(/\s+/g, '').length; if (textLen < 100) return; if (s > bestScore) { bestScore = s; best = el; } }); if (!best || bestScore < 60) return null; if (best === doc.body || best === doc.documentElement) return null; const cn = ((best.className || '') + ' ' + (best.id || '')).trim(); // console.log(`[阅读模式] 评分器命中:<${best.tagName.toLowerCase()}${cn ? ' class="' + cn + '"' : ''}> 得分 ${bestScore.toFixed(1)}`); return best; } 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 = [], 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: '//*[text()="目录" or text()="章节目录" or text()="返回目录" or text()="书籍页" or 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: ['上一章', '上一页', '上一篇'], exact: false }, { key: 'next', texts: ['下一章', '下一页', '下一篇'], exact: false }, { key: 'index', texts: ['目录', '章节目录', '返回目录', '书籍页', '章节列表'], exact: true } ]; for (const item of containers) { if (!res[item.key]) { for (const txt of item.texts) { const xpath = item.exact ? `//*[text()="${txt}"]` : `//*[contains(text(),"${txt}")]`; const container = doc.evaluate(xpath, 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 extractNavFromScriptVars(doc) { const res = { prev: null, next: null, index: null }; let win = null; try { win = (doc && doc.defaultView) || (doc === document ? window : null); } catch(e) { win = null; } if (!win) return res; const VAR_NAMES = ['ReadParams', 'readParams', 'bookParams', 'chapterParams', 'novelParams', 'chapterInfo', 'readInfo', 'novelInfo']; const PREV_KEYS = ['url_previous', 'url_prev', 'url_pre', 'prevUrl', 'prev_url', 'previousUrl', 'previous_url', 'pre_url', 'preUrl']; const NEXT_KEYS = ['url_next', 'urlNext', 'nextUrl', 'next_url', 'url_nextpage', 'nextPageUrl']; const INDEX_KEYS = ['url_index', 'url_catalog', 'url_catelog', 'indexUrl', 'index_url', 'catalogUrl', 'catalog_url', 'muluUrl', 'url_mulu', 'url_list']; const pickUrl = (obj, keys) => { for (const k of keys) { let v; try { v = obj[k]; } catch(e) { continue; } if (typeof v === 'string' && v.trim()) { const s = v.trim(); if (s === '#' || s.startsWith('javascript:')) continue; try { return new URL(s, win.location.href).href; } catch(e) { return s; } } } return null; }; const makeFakeLink = (href, text) => ({ href, text, textContent: text, tagName: 'A', nodeType: 1, __fromVars: true }); for (const name of VAR_NAMES) { let obj; try { obj = win[name]; } catch(e) { continue; } if (!obj || typeof obj !== 'object') continue; if (!res.prev) { const u = pickUrl(obj, PREV_KEYS); if (u && isValidHref(u)) res.prev = makeFakeLink(u, '上一页'); } if (!res.next) { const u = pickUrl(obj, NEXT_KEYS); if (u && isValidHref(u)) res.next = makeFakeLink(u, '下一页'); } if (!res.index) { const u = pickUrl(obj, INDEX_KEYS); if (u && isValidHref(u)) res.index = makeFakeLink(u, '目录'); } if (res.prev && res.next && res.index) break; } if (!res.prev || !res.next || !res.index) { let inlineText = ''; try { const scripts = doc.querySelectorAll ? doc.querySelectorAll('script:not([src])') : []; for (const s of scripts) { const t = s.textContent || ''; if (/url_(previous|next|index|prev|pre)/i.test(t) || /(prevUrl|nextUrl|catalogUrl|indexUrl)/i.test(t)) { inlineText += t + '\n'; } } } catch(e) {} if (inlineText) { const grab = (keys) => { for (const k of keys) { const re = new RegExp(k + "\\s*:\\s*['\"]([^'\"]+)['\"]", 'i'); const m = inlineText.match(re); if (m && m[1] && m[1].trim() && m[1] !== '#' && !m[1].startsWith('javascript:')) { try { return new URL(m[1].trim(), win.location.href).href; } catch(e) { return m[1].trim(); } } } return null; }; if (!res.prev) { const u = grab(PREV_KEYS); if (u && isValidHref(u)) res.prev = makeFakeLink(u, '上一页'); } if (!res.next) { const u = grab(NEXT_KEYS); if (u && isValidHref(u)) res.next = makeFakeLink(u, '下一页'); } if (!res.index) { const u = grab(INDEX_KEYS); if (u && isValidHref(u)) res.index = makeFakeLink(u, '目录'); } } } return res; } const INDEX_STANDARD_TEXTS = new Set([ '目录', '章节目录', '返回目录', '书籍页', '章节列表', '全部目录', '完整目录', '查看目录', '本书目录', '目录页', '返回书目', '目録', '目錄', '返回目錄', '章節目錄' ]); function isIndexLinkAcceptable(a) { if (!a || !a.href || !isValidHref(a.href)) return false; const t = (a.textContent || '').trim(); if (INDEX_STANDARD_TEXTS.has(t)) return true; if (t.includes('目录') && INDEX_URL_HINTS.test(a.href)) return true; return false; } 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; const sv = extractNavFromScriptVars(doc); if (!links.prev && sv.prev) { links.prev = sv.prev; // console.log(`[阅读模式] 变量兜底 prev: ${sv.prev.href}`); } if (!links.next && sv.next) { links.next = sv.next; // console.log(`[阅读模式] 变量兜底 next: ${sv.next.href}`); } if (!links.index && sv.index) { links.index = sv.index; // console.log(`[阅读模式] 变量兜底 index: ${sv.index.href}`); } if (links.index && !isIndexLinkAcceptable(links.index)) { // console.log(`[阅读模式] 目录候选被过滤(文本/URL 不符):"${(links.index.textContent || '').trim()}" → ${links.index.href}`); links.index = null; } try { const indexCandidates = collectLinksByText(doc, NAV_SELECTORS.index).filter(isIndexLinkAcceptable); if (cl.index && isIndexLinkAcceptable(cl.index) && !indexCandidates.some(a => a.href === cl.index.href)) indexCandidates.push(cl.index); if (links.index && links.index.__fromVars && isValidHref(links.index.href) && !indexCandidates.some(a => a.href === links.index.href)) { indexCandidates.push(links.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) {} const isParentPath = (href) => { if (!href) return false; try { const link = new URL(href, window.location.href); const cur = new URL(window.location.href); if (link.origin !== cur.origin) return false; const toDir = p => p.replace(/\/[^/]*$/, '/').replace(/\/+$/, '/'); return link.pathname.replace(/\/+$/, '/') === toDir(cur.pathname); } catch(e) { return false; } }; ['prev', 'next'].forEach(key => { const l = links[key]; if (l && isParentPath(l.href)) { if (!links.index) links.index = l; links[key] = null; } }); function isSameChapterUrl(curHref, tgtHref) { if (!curHref || !tgtHref) return false; try { const cur = new URL(curHref); const tgt = new URL(tgtHref); if (cur.origin !== tgt.origin) return false; const curNums = cur.pathname.match(/\d+/g) || []; const tgtNums = tgt.pathname.match(/\d+/g) || []; if (curNums.length === 0 || tgtNums.length === 0) return false; if (curNums.length === tgtNums.length && curNums.every((n, i) => n === tgtNums[i])) return true; if (tgtNums.length > curNums.length) { const suffix = tgtNums.slice(-curNums.length); if (suffix.every((n, i) => n === curNums[i])) return true; } return false; } catch(e) { return false; } } let curHrefForDedup = window.location.href; try { if (doc && doc !== document && doc.defaultView && doc.defaultView.location) { curHrefForDedup = doc.defaultView.location.href; } } catch(e) {} const indexKeyForDedup = (links.index && links.index.href) ? normalizeUrl(links.index.href) : null; ['prev', 'next'].forEach(key => { const l = links[key]; if (!l || !l.href) return; if (isSameChapterUrl(curHrefForDedup, l.href)) { links[key] = null; return; } if (indexKeyForDedup && normalizeUrl(l.href) === indexKeyForDedup) { links[key] = null; } }); return links; } function extractContent(doc) { doc = doc || document; let contentElement = null; let fromSelector = false; for (const s of CONTENT_SELECTORS) { let els; try { els = doc.querySelectorAll(s); } catch(e) { continue; } if (els.length) { let max = els[0], 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; fromSelector = true; // console.log(`[阅读模式] 使用选择器 "${s}"`); break; } } if (!contentElement) { // console.log('[阅读模式] 白名单未命中,启用评分器'); contentElement = findContentByScoring(doc); fromSelector = false; } if (!contentElement) return null; const autoHasIndent = detectIndent(contentElement); const hiddenPaths = collectHiddenPaths(contentElement); const clone = contentElement.cloneNode(true); removeByPaths(clone, hiddenPaths); pruneCandidates(clone); cleanContent(clone, autoHasIndent); let title = ''; let titleFromSelector = false; 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(); titleFromSelector = true; // console.log(`[阅读模式] 使用标题选择器 "${s}" 匹配到标题: "${title}"`); break; } } if (!title) { title = doc.title || ''; /* console.log(`[阅读模式] 使用网页标题: "${title}"(不参与正文去重)`); */ } if (titleFromSelector) removeDuplicateTitle(clone, title); const html = clone.innerHTML.trim(); const textContent = clone.textContent.trim(); if (!html || !textContent || textContent.length <= MIN_CONTENT_LENGTH) return null; const nav = findNavLinks(doc); return { title, content: html, nav, autoHasIndent, fromSelector }; } function addReaderModeHash(url) { try { const u = new URL(url, window.location.origin); u.hash = 'readermode'; return u.href; } catch(e) { return (url || '').split('#')[0] + '#readermode'; } } function stripReaderModeHash(url) { try { const u = new URL(url, window.location.origin); u.hash = ''; return u.href; } catch(e) { return (url || '').split('#')[0]; } } 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; return (0.299 * r + 0.587 * g + 0.114 * b) / 255 < 0.5; } const getDividerColor = actualBg => isDarkColor(actualBg) ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)'; 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: getDividerColor(bg), 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)' }; } const getEffectiveBgColor = (userBg, isNight) => isNight ? NIGHT_THEME.bgColor : userBg; const getAccentColor = (userBg, isNight) => isNight ? NIGHT_THEME.navBtnColor : (isDarkColor(userBg) ? '#4a9eff' : '#007bff'); const getIndentRule = autoHasIndent => autoHasIndent ? '.reader-chapter-content p, .reader-chapter-content li{text-indent:2em;}' : '.reader-chapter-content p, .reader-chapter-content li{text-indent:0;}'; function buildContentStyleCSS(userBg, autoHasIndent, isNight) { const colors = calculateColors(userBg, isNight); const indentRule = getIndentRule(autoHasIndent); return ` .reader-chapter-content p, .reader-chapter-content div, .reader-chapter-content h1, .reader-chapter-content h2, .reader-chapter-content h3, .reader-chapter-content h4, .reader-chapter-content h5, .reader-chapter-content h6, .reader-chapter-content li, .reader-chapter-content blockquote, .reader-chapter-content pre, .reader-chapter-content table, .reader-chapter-content td, .reader-chapter-content th { max-width: 100%; overflow-wrap: break-word; word-wrap: break-word; word-break: break-all; box-sizing: border-box; } .reader-chapter-content p { text-align: justify; margin: 0 0 1.8em 0; white-space: normal !important; } ${indentRule} .reader-chapter-content p[align] { text-indent: 0; } .reader-chapter-content p[align="left"] { text-align: left; } .reader-chapter-content p[align="center"] { text-align: center; } .reader-chapter-content p[align="right"] { text-align: right; } .reader-chapter-content img { max-width: 100%; height: auto; } .reader-chapter-content img[data-emoji] { display: inline; max-width: none; width: 1.2em; height: 1.2em; vertical-align: -0.15em; } .reader-chapter-content p > img:only-child, .reader-chapter-content div > img:only-child { display: block; margin: 1.2em auto; } .reader-chapter-content blockquote { margin-left: 0; padding-left: 1.2em; border-left: 3px solid ${colors.borderColor}; color: ${colors.subtleTextColor}; } .reader-chapter-content a { color: ${colors.navBtnColor}; text-decoration: none; cursor: pointer; } .reader-chapter-content a:hover { text-decoration: none; } .reader-chapter-content strong, .reader-chapter-content b { font-weight: bold; } .reader-chapter-content em, .reader-chapter-content i { font-style: italic; } .reader-chapter-content h1, .reader-chapter-content h2, .reader-chapter-content h3, .reader-chapter-content h4, .reader-chapter-content h5, .reader-chapter-content h6 { font-weight: bold; margin-top: 1.2em; margin-bottom: 0.6em; } .reader-chapter-content h1 { font-size: 1.8em; } .reader-chapter-content h2 { font-size: 1.5em; margin-top: 2em; margin-bottom: 0.8em; } .reader-chapter-content h3 { font-size: 1.3em; } .reader-chapter-content ul, .reader-chapter-content ol { padding-left: 0; margin: 0.5em 0; list-style: none; } .reader-chapter-content li { display: block; list-style: none; margin: 0.2em 0; padding-left: 1.2em; position: relative; } .reader-chapter-content ul > li::before { content: ''; position: absolute; left: 0.2em; top: 0.85em; width: 5px; height: 5px; border-radius: 50%; background: currentColor; } .reader-chapter-content 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-chapter-content 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-chapter-content pre code { background: none; border: none; padding: 0; border-radius: 0; font-size: inherit; color: inherit; } .reader-chapter-content table { border-collapse: collapse; width: 100%; margin: 1em 0; } .reader-chapter-content th, .reader-chapter-content td { border: 1px solid ${colors.tableBorder}; padding: 6px 10px; text-align: left; } .reader-chapter-content th { background: ${colors.tableHeaderBg}; font-weight: 600; } .reader-chapter-block + .reader-chapter-block { margin-top: 60px; } `; } 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, 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*[章节回卷话集]/); return m ? chineseNumToArabic(m[1]) : null; } const PRE_CHAPTER_RE = /^(楔子|序章|序言|序篇|序曲|序幕|引子|引言|前言|开篇|题记|prologue|プロローグ)/i; function isPreChapterText(text) { if (!text) return false; const t = (text || '').replace(/[\s\u3000【】\[\]()()::、,.,。·\-—_~~!!??]+/g, '').toLowerCase(); if (!t || t.length > 6) return false; return PRE_CHAPTER_RE.test(t); } function liftPreChapters(links) { if (!links || links.length < 2) return links; const pre = []; const rest = []; for (const l of links) { if (l && !l.__missing && isPreChapterText(l.text)) pre.push(l); else rest.push(l); } if (pre.length === 0) return links; return pre.concat(rest); } 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', '#volumes', '.volume-chapters' ]; function findBestContainer(root) { let container = null, 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 = '', 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; const VOLUME_URL_RE = /(?:^|\/)(?:vol|volume)[-_]?\d+(?:\.html?)?\/?$/i; const VOLUME_PARENT_CLS_RE = /(^|[\s_-])(chapter-bar|volume-cover|vol-cover|volume-header|vol-header|book-cover)([\s_-]|$)/i; function isChapterCandidate(a) { const href = a.getAttribute('href'); if (!href || href === '#' || href.startsWith('javascript:')) return false; try { const absObj = new URL(href, indexUrl); if (VOLUME_URL_RE.test(absObj.pathname)) return false; } catch(e) {} if (a.closest) { const parentLi = a.closest('li'); if (parentLi) { const pcls = ((parentLi.className || '') + ' ' + (parentLi.id || '')).trim(); if (pcls && VOLUME_PARENT_CLS_RE.test(pcls)) 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, 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], 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) {} } result = liftPreChapters(result); 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} 条章节链接`); 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) { return { doc: null, error: e.name === 'SecurityError' ? 'cross-origin' : 'unknown' }; } } async function fetchDocTemp(url, timeoutMs) { const controller = new AbortController(); const t = setTimeout(() => controller.abort(), timeoutMs || 10000); try { const resp = await fetch(url, { credentials: 'include', signal: controller.signal }); clearTimeout(t); if (!resp.ok) return null; const html = await resp.text(); if (!html || html.length < 100) return null; return new DOMParser().parseFromString(html, 'text/html'); } catch (e) { clearTimeout(t); return null; } } function loadDocTemp(url, timeoutMs) { return new Promise((resolve) => { const iframe = createSandboxIframe(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(() => { fetchDocTemp(url, timeoutMs).then((doc) => cleanup(doc)).catch(() => cleanup(null)); }, timeoutMs || 12000); iframe.onload = () => { clearTimeout(t); const result = safeGetIframeDoc(iframe); if (result.doc && result.doc.body && result.doc.querySelectorAll('a').length > 0) { cleanup(result.doc); } else { fetchDocTemp(url, timeoutMs).then((doc) => cleanup(doc)).catch(() => cleanup(null)); } }; document.body.appendChild(iframe); }); } function fetchChapterArticleViaIframe(url, timeoutMs) { return new Promise((resolve) => { const iframe = createSandboxIframe(url); let done = false; let pollTimer = null; const cleanup = (article) => { if (done) return; done = true; if (pollTimer) clearTimeout(pollTimer); try { if (iframe.parentNode) iframe.remove(); } catch(e) {} resolve(article); }; const timeout = setTimeout(() => cleanup(null), timeoutMs || 12000); iframe.onload = () => { clearTimeout(timeout); const result = safeGetIframeDoc(iframe); if (!result.doc) { cleanup(null); return; } let pollCount = 0; const MAX_POLL = 15, INTERVAL = 200; const tryExtract = () => { if (done) return; let article = null; try { article = extractContent(result.doc); } catch(e) {} if (article) { cleanup(article); return; } pollCount++; if (pollCount < MAX_POLL) pollTimer = setTimeout(tryExtract, INTERVAL); else cleanup(null); }; tryExtract(); }; document.body.appendChild(iframe); }); } function isSameChapterPager(currentUrl, nextUrl) { if (!currentUrl || !nextUrl) return false; try { const cur = new URL(currentUrl); const nxt = new URL(nextUrl); if (cur.origin !== nxt.origin) return false; const curPath = cur.pathname; const nxtPath = nxt.pathname; const curBase = curPath.match(/^(.*\/)(\d+)(?:_(\d+))?\.html?$/i); const nxtBase = nxtPath.match(/^(.*\/)(\d+)_(\d+)\.html?$/i); if (!curBase || !nxtBase) return false; if (curBase[1] !== nxtBase[1]) return false; if (curBase[2] !== nxtBase[2]) return false; const curPage = curBase[3] ? parseInt(curBase[3], 10) : 1; const nxtPage = parseInt(nxtBase[3], 10); return nxtPage === curPage + 1; } catch(e) { return false; } } async function fetchChapterArticle(url, depth) { depth = depth || 0; if (depth > 6) return null; let article = await fetchChapterArticleViaIframe(url, 12000); if (!article) { // console.log('[阅读模式] iframe 通道未取到正文,fetch 兜底:', url); const doc = await fetchDocTemp(url, 12000); if (!doc) return null; try { article = extractContent(doc); } catch(e) { return null; } if (!article) return null; } if (article.nav && article.nav.next && isSameChapterPager(url, article.nav.next.href)) { const nextArticle = await fetchChapterArticle(article.nav.next.href, depth + 1); if (nextArticle) { return { ...article, content: article.content + nextArticle.content, nav: { prev: article.nav.prev, next: nextArticle.nav.next, index: nextArticle.nav.index || article.nav.index, indexCandidates: nextArticle.nav.indexCandidates || article.nav.indexCandidates } }; } } return article; } 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, cancelled: false }; currentPreloadTask = task; (async () => { try { const article = await fetchChapterArticle(url); if (task.cancelled) return; if (article) { chapterCache.set(cacheKey, { article, timestamp: Date.now() }); if (chapterCache.size > 2) { const firstKey = chapterCache.keys().next().value; chapterCache.delete(firstKey); } } } catch(e) {} if (currentPreloadTask === task) currentPreloadTask = null; })(); } function cancelCurrentPreload() { if (!currentPreloadTask) return; currentPreloadTask.cancelled = true; 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 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.warn('[阅读模式] 目录预加载失败: 未抓取到章节链接'); 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 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 consider = (absUrl) => { const abs = absUrl.href; if (normalizeUrl(abs) === baseKey) return; if (!absUrl.pathname.startsWith(pathBase)) return; const path = absUrl.pathname, 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); }; 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 { consider(new URL(href, baseUrl)); } catch(e) {} }); root.querySelectorAll('select option[value]').forEach(opt => { const href = opt.value; if (!href || href === '#' || href.startsWith('javascript:')) return; if (!isValidHref(href)) return; try { consider(new URL(href, baseUrl)); } 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', '.page']; 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 { pushLinks(extractChapterLinksFromDoc(doc, url)); } 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, depth = 0; while (cur && depth < 3) { try { dispatchRealClick(cur); } catch(e) {} cur = cur.parentElement; depth++; } } }; fireClicks(); let pollCount = 0; const MAX_POLLS = 28, POLL_INTERVAL = 250, 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 = createSandboxIframe(indexUrl); let firstLoaded = false; const runFallbackFetch = async () => { if (finalized) return; // console.log('[阅读模式] iframe 加载失败或被拒,fetch 兜底'); const doc = await fetchDocTemp(indexUrl, 12000); if (finalized) return; if (!doc) { clearTimeout(hardTimeout); finalize([]); return; } let firstLinks = []; try { firstLinks = extractChapterLinksFromDoc(doc, indexUrl); } catch(e) {} const pagerUrls = findPagerUrls(doc, indexUrl); if (pagerUrls.length > 0) { runPagerStrategy(pagerUrls, firstLinks); return; } clearTimeout(hardTimeout); finalize(firstLinks); }; const firstTimeout = setTimeout(() => { if (!firstLoaded) { firstLoaded = true; if (firstIframe.parentNode) firstIframe.remove(); runFallbackFetch(); } }, 15000); firstIframe.onload = () => { if (finalized) return; firstLoaded = true; clearTimeout(firstTimeout); const result = safeGetIframeDoc(firstIframe); if (!result.doc || !result.doc.body || result.doc.querySelectorAll('a').length === 0) { if (firstIframe.parentNode) firstIframe.remove(); runFallbackFetch(); 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 cleanReaderContent(overlay) { if (!overlay || !overlay.isConnected) return; overlay.querySelectorAll('.reader-chapter-content').forEach(sec => cleanPageAds(sec)); } function scheduleReaderContentClean(overlay) { if (!overlay) return; if (overlay.__cleanTimers) { overlay.__cleanTimers.forEach(t => clearTimeout(t)); } overlay.__cleanTimers = []; cleanReaderContent(overlay); [100, 300, 800].forEach(t => { const timer = setTimeout(() => { if (overlay.isConnected) cleanReaderContent(overlay); }, t); overlay.__cleanTimers.push(timer); }); } 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% - 9px)'; return; } const total = catalogCache.links.length; if (total <= 1) { thumb.style.left = 'calc(100% - 9px)'; 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}% - 9px)`; 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) { thumb.style.left = `calc(${(curNum / maxNum) * 100}% - 9px)`; return; } thumb.style.left = 'calc(0% - 9px)'; } 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 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 12px; color: ${colors.subtleTextColor}; font-size: 15px; 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 12px; color: ${isCurrent ? colors.navBtnColor : colors.textColor}; text-decoration: none; font-size: 15px; font-weight: ${isCurrent ? '600' : 'normal'}; background: 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', () => a.style.background = 'transparent'); if (onLinkClick) a.addEventListener('click', (e) => { e.preventDefault(); onLinkClick(link.href); }); container.appendChild(a); }); } function refreshCatalogColors(userBg, isNight) { if (!currentCatalogView) return; const actualBg = getEffectiveBgColor(userBg, isNight); currentCatalogView.style.backgroundColor = actualBg; const catalogColors = calculateColors(userBg, isNight); currentCatalogView.querySelectorAll('a').forEach(a => { const isCurrent = a.dataset.current === 'true'; a.style.color = isCurrent ? catalogColors.navBtnColor : catalogColors.textColor; a.style.background = 'transparent'; 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; 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; } 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 lineColor = getDividerColor(actualBg); const header = document.createElement('div'); header.style.cssText = `display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; flex-shrink: 0; border-bottom: 1px solid ${lineColor};`; 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: 13px; 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;`; 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}; 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 }); function autoScrollToCurrent() { if (catalogOrder !== 'asc') return; const currentEl = listContainer.querySelector('a[data-current="true"]'); if (!currentEl) return; try { const listRect = listContainer.getBoundingClientRect(); const elRect = currentEl.getBoundingClientRect(); listContainer.scrollTop = listContainer.scrollTop + (elRect.top - listRect.top); } catch(e) { try { currentEl.scrollIntoView({ block: 'start', behavior: 'auto' }); } catch(e2) {} } } const scheduleAutoLocate = () => { requestAnimationFrame(() => requestAnimationFrame(autoScrollToCurrent)); }; 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(); scheduleAutoLocate(); 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(); scheduleAutoLocate(); } 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); (async () => { const doc = await loadDocTemp(indexUrl, 8000); const el = overlay.querySelector('#reader-catalog-loading'); if (el) el.remove(); if (!doc) { renderError(listContainer, '目录页无法访问(iframe 与 fetch 均失败)。', indexUrl, colors); return; } try { const links = extractChapterLinksFromDoc(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(); scheduleAutoLocate(); } catch (e) { // console.error('[阅读模式] 解析目录失败:', e); renderError(listContainer, '解析目录失败。', indexUrl, colors); } })(); 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 lineColor = getDividerColor(actualBg); const header = document.createElement('div'); header.style.cssText = `display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid ${lineColor}; 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: 16px; 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() { const colors = (() => { const nowIsNight = overlay.__isNight === true; const nowUserBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); return calculateColors(nowUserBg, nowIsNight); })(); 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(10, 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(); panel.__refreshColors = () => { const nowIsNight = overlay.__isNight === true; const nowUserBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); const nowActualBg = getEffectiveBgColor(nowUserBg, nowIsNight); const nowColors = calculateColors(nowUserBg, nowIsNight); panel.style.backgroundColor = nowActualBg; header.style.borderBottom = `1px solid ${getDividerColor(nowActualBg)}`; titleSpan.style.color = nowColors.textColor; closeBtn.style.color = nowColors.subtleTextColor; renderMainView(); }; overlay.appendChild(panel); requestAnimationFrame(() => { panel.style.transform = 'translateY(0)'; mask.style.opacity = '1'; }); } function applyImmersiveTopStyle(el, colors, style, visible) { if (!el) return; const transform = visible ? 'translateY(0)' : 'translateY(-100%)'; const opacity = visible ? '1' : '0'; 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: 12px 16px; border-radius: 0; box-shadow: none;`; } 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'; 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: manipulation; 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-radius: 0; box-shadow: none;`; } 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); if (botEl && typeof botEl.__setImmersiveVisible === 'function') botEl.__setImmersiveVisible(show); } function createImmersiveTopBar(title, colors, onExit, 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; justify-content: center; cursor: pointer; padding: 8px; 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: 16px; font-weight: 600; color: ${colors.textColor}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0 8px; transition: color 0.3s ease;`; const spacer = document.createElement('div'); spacer.style.cssText = 'width: 36px; height: 36px; flex-shrink: 0; pointer-events: none;'; bar.appendChild(back); bar.appendChild(titleEl); bar.appendChild(spacer); 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); let lastContextMenuTime = 0; const isAfterContextMenu = () => Date.now() - lastContextMenuTime < 800; const row1 = document.createElement('div'); row1.className = 'rib-row1'; const prevBtn = document.createElement('a'); prevBtn.className = 'rt-btn'; prevBtn.id = 'reader-prev-btn'; prevBtn.textContent = '上一章'; if (nav && nav.prev) prevBtn.href = addReaderModeHash(nav.prev.href); 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; text-decoration: none; transition: color 0.3s ease; touch-action: manipulation; -webkit-tap-highlight-color: transparent; -webkit-touch-callout: default;`; prevBtn.addEventListener('contextmenu', () => { lastContextMenuTime = Date.now(); }); prevBtn.addEventListener('click', (e) => { e.preventDefault(); if (isAfterContextMenu()) return; 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: 9px; background: ${isDarkColor(colors.controlBgColor) ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}; border-radius: 4.5px; 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% - 9px); top: 50%; transform: translateY(-50%); width: 18px; height: 18px; background: ${colors.navBtnColor}; border-radius: 50%; box-shadow: 0 1px 4px rgba(0,0,0,0.3); transition: left 0.3s ease; z-index: 3;`; progressContainer.appendChild(progressThumb); const progressMarkerA = document.createElement('div'); progressMarkerA.className = 'reader-progress-marker-a'; progressMarkerA.style.cssText = `position: absolute; top: 50%; left: 0; width: 10px; height: 10px; background: #999; border-radius: 50%; transform: translate(-50%, -50%); pointer-events: none; opacity: 0; transition: left 0.2s ease, opacity 0.2s ease; z-index: 2;`; progressContainer.appendChild(progressMarkerA); const markerAHitArea = document.createElement('div'); markerAHitArea.style.cssText = `position: absolute; top: 50%; left: 0; width: 44px; height: 44px; transform: translate(-50%, -50%); border-radius: 50%; cursor: pointer; display: none; z-index: 4; -webkit-tap-highlight-color: transparent;`; progressContainer.appendChild(markerAHitArea); const progressBubble = document.createElement('div'); progressBubble.className = 'reader-progress-bubble'; progressBubble.style.cssText = `position: absolute; bottom: calc(100% + 14px); left: 50%; padding: 10px 20px; background: rgba(28,28,30,0.85); color: #fff; border-radius: 999px; font-size: 13px; font-weight: 500; line-height: 1.4; white-space: nowrap; pointer-events: none; max-width: 80vw; overflow: hidden; text-overflow: ellipsis; opacity: 0; transform: translateX(-50%) translateY(6px); transition: opacity 0.22s ease, transform 0.22s cubic-bezier(0.2, 0.8, 0.2, 1); z-index: 100; box-shadow: 0 8px 24px rgba(0,0,0,0.2); font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; text-align: center; word-break: break-word;`; const bubblePercentNode = document.createElement('div'); bubblePercentNode.style.cssText = 'font-size: 12px; color: rgba(255,255,255,0.75); margin-bottom: 2px;'; progressBubble.appendChild(bubblePercentNode); const bubbleChapterNode = document.createElement('div'); bubbleChapterNode.style.cssText = 'font-size: 13px; font-weight: 500; color: #fff; overflow: hidden; text-overflow: ellipsis;'; progressBubble.appendChild(bubbleChapterNode); progressContainer.appendChild(progressBubble); let isDraggingProgress = false, markerAPosition = null, hasSnappedToA = false, bubbleHideTimer = null; const canDragProgress = () => catalogCache && catalogCache.links && catalogCache.links.length > 1; const SNAP_THRESHOLD_PX = 10; 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)); if (markerAPosition !== null) { const distPx = Math.abs(percent - markerAPosition) * rect.width; if (distPx <= SNAP_THRESHOLD_PX) { percent = markerAPosition; if (!hasSnappedToA) { hasSnappedToA = true; if (navigator.vibrate) { try { navigator.vibrate(15); } catch (e) {} } } } else { hasSnappedToA = false; } } progressThumb.style.transition = 'none'; progressThumb.style.left = `calc(${percent * 100}% - 9px)`; return percent; }; const showBubble = (percent) => { if (!catalogCache || !catalogCache.links || !catalogCache.links.length) return; const total = catalogCache.links.length; const idx = Math.round(percent * (total - 1)); const link = catalogCache.links[idx]; if (!link) return; let chapterName = link.text || '未知章节'; if (chapterName.length > 15) chapterName = chapterName.slice(0, 15) + '…'; bubblePercentNode.textContent = Math.round(percent * 100) + '%'; bubbleChapterNode.textContent = chapterName; progressBubble.style.opacity = '1'; progressBubble.style.transform = 'translateX(-50%) translateY(0)'; }; const hideBubble = () => { progressBubble.style.opacity = '0'; progressBubble.style.transform = 'translateX(-50%) translateY(6px)'; }; const setMarkerA = (percent) => { markerAPosition = percent; progressMarkerA.style.left = `calc(${percent * 100}%)`; markerAHitArea.style.left = `calc(${percent * 100}%)`; progressMarkerA.style.opacity = '1'; markerAHitArea.style.display = 'block'; }; const clearMarkerA = () => { markerAPosition = null; hasSnappedToA = false; progressMarkerA.style.opacity = '0'; markerAHitArea.style.display = 'none'; }; const tryJumpToA = () => { if (markerAPosition === null) return; if (!catalogCache || !catalogCache.links || !catalogCache.links.length) return; const total = catalogCache.links.length; const idx = Math.round(markerAPosition * (total - 1)); const target = catalogCache.links[idx]; if (!target || target.__missing || !target.href) return; if (onChapterNav) onChapterNav(target.href); else window.location.href = target.href; }; markerAHitArea.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); tryJumpToA(); }); 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; const percent = updateThumbByX(e.touches[0].clientX); if (markerAPosition === null) { setMarkerA(percent); hasSnappedToA = true; } else { hasSnappedToA = false; } if (bubbleHideTimer) { clearTimeout(bubbleHideTimer); bubbleHideTimer = null; } showBubble(percent); e.stopPropagation(); }; const onProgressTouchMove = (e) => { if (!isDraggingProgress) return; if (!e.touches || !e.touches.length) return; const percent = updateThumbByX(e.touches[0].clientX); showBubble(percent); 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); bubbleHideTimer = setTimeout(() => { hideBubble(); bubbleHideTimer = null; }, 2000); }; const onProgressMouseDown = (e) => { if (!canDragProgress()) return; isDraggingProgress = true; const percent = updateThumbByX(e.clientX); if (markerAPosition === null) { setMarkerA(percent); hasSnappedToA = true; } else { hasSnappedToA = false; } if (bubbleHideTimer) { clearTimeout(bubbleHideTimer); bubbleHideTimer = null; } showBubble(percent); e.stopPropagation(); }; const onProgressMouseMove = (e) => { if (!isDraggingProgress) return; const percent = updateThumbByX(e.clientX); showBubble(percent); }; const onProgressMouseUp = (e) => { if (!isDraggingProgress) return; isDraggingProgress = false; finishProgressDrag(e.clientX); bubbleHideTimer = setTimeout(() => { hideBubble(); bubbleHideTimer = null; }, 2000); }; 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); if (bubbleHideTimer) { clearTimeout(bubbleHideTimer); bubbleHideTimer = null; } }; bar.__setImmersiveVisible = (visible) => { if (visible) { if (markerAPosition !== null) { progressMarkerA.style.opacity = '1'; markerAHitArea.style.display = 'block'; } } else { clearMarkerA(); hideBubble(); if (bubbleHideTimer) { clearTimeout(bubbleHideTimer); bubbleHideTimer = null; } } }; const nextBtn = document.createElement('a'); nextBtn.className = 'rt-btn'; nextBtn.id = 'reader-next-btn'; nextBtn.textContent = '下一章'; if (nav && nav.next) nextBtn.href = addReaderModeHash(nav.next.href); 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; text-decoration: none; transition: color 0.3s ease; touch-action: manipulation; -webkit-tap-highlight-color: transparent; -webkit-touch-callout: default;`; nextBtn.addEventListener('contextmenu', () => { lastContextMenuTime = Date.now(); }); nextBtn.addEventListener('click', (e) => { e.preventDefault(); if (isAfterContextMenu()) return; 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; height: 72px; padding: 0 16px;'; function makeIconBtn(iconSvg, label, onClick, disabled, action, href) { const useLink = !!href && !disabled; const btn = document.createElement(useLink ? 'a' : 'div'); btn.className = 'rt-icon-btn'; btn.dataset.accent = 'true'; btn.dataset.action = action || ''; if (disabled) btn.dataset.disabled = 'true'; if (useLink) { btn.href = href; btn.style.textDecoration = 'none'; btn.style.touchAction = 'manipulation'; btn.style.webkitTouchCallout = 'default'; } const baseColor = disabled ? colors.subtleTextColor : colors.textColor; btn.style.cssText += `;display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; 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('contextmenu', () => { lastContextMenuTime = Date.now(); }); btn.addEventListener('click', (e) => { if (useLink) e.preventDefault(); if (isAfterContextMenu()) return; 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', nav && nav.index ? nav.index.href : null)); 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: 4px; 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'); const catalogBtn = overlay.querySelector('#reader-immersive-bottom .rt-icon-btn[data-action="catalog"]'); if (prevBtn) { prevBtn.style.cursor = nav && nav.prev ? 'pointer' : 'not-allowed'; prevBtn.style.opacity = nav && nav.prev ? '1' : '0.4'; if (prevBtn.tagName === 'A') { if (nav && nav.prev) prevBtn.href = addReaderModeHash(nav.prev.href); else prevBtn.removeAttribute('href'); } } if (nextBtn) { nextBtn.style.cursor = nav && nav.next ? 'pointer' : 'not-allowed'; nextBtn.style.opacity = nav && nav.next ? '1' : '0.4'; if (nextBtn.tagName === 'A') { if (nav && nav.next) nextBtn.href = addReaderModeHash(nav.next.href); else nextBtn.removeAttribute('href'); } } if (catalogBtn && catalogBtn.tagName === 'A') { if (nav && nav.index) catalogBtn.href = nav.index.href; else catalogBtn.removeAttribute('href'); } } 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) { 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;overscroll-behavior-y:contain;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;'; const style = document.createElement('style'); style.id = 'reader-content-style'; style.textContent = buildContentStyleCSS(initialUserBg, article.autoHasIndent, isNight); mainContainer.appendChild(style); overlay.__nav = article.nav; overlay.__currentUrl = window.location.href; const chapterState = { list: [], currentUrl: window.location.href, isAppending: false, isJumping: false }; const getNowColors = () => { const nowIsNight = overlay.__isNight === true; const nowUserBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor); return calculateColors(nowUserBg, nowIsNight); }; const getNowFontSize = () => mainContainer.style.fontSize || initialFontSize; function createChapterBlock(art, url) { const colors = getNowColors(); const fontSize = getNowFontSize(); const block = document.createElement('div'); block.className = 'reader-chapter-block'; block.dataset.url = url; if (art.title && art.title.trim()) { const title = document.createElement('h1'); title.className = 'reader-chapter-title'; title.textContent = art.title; title.style.cssText = `margin:0 0 30px 0;font-size:1.4em;font-weight:700;line-height:1.4;color:${colors.textColor};text-align:left;user-select:text;overflow-wrap:break-word;word-wrap:break-word;`; block.appendChild(title); } const content = document.createElement('div'); content.className = 'reader-chapter-content'; content.innerHTML = art.content; content.style.cssText = `font-size:${fontSize};line-height:1.8;color:${colors.textColor};user-select:text;overflow-wrap:break-word;word-wrap:break-word;max-width:100%;text-align:left;`; block.appendChild(content); return block; } async function appendNextChapter() { if (chapterState.isAppending || chapterState.isJumping) return; const last = chapterState.list[chapterState.list.length - 1]; if (!last || !last.article || !last.article.nav || !last.article.nav.next) return; const nextUrl = last.article.nav.next.href; if (!nextUrl) return; chapterState.isAppending = true; const loadingHint = document.createElement('div'); loadingHint.className = 'reader-append-loading'; loadingHint.textContent = '正在加载...'; const hintColors = getNowColors(); loadingHint.style.cssText = `text-align:center; padding:40px 0; color:${hintColors.subtleTextColor}; font-size:14px;`; contentArea.appendChild(loadingHint); try { const nextKey = normalizeUrl(nextUrl); const cached = chapterCache.get(nextKey); let art = null; if (cached && cached.article) { chapterCache.delete(nextKey); art = cached.article; } else { art = await fetchChapterArticle(nextUrl); } if (!overlay.isConnected) return; if (!art) return; const block = createChapterBlock(art, nextUrl); contentArea.appendChild(block); chapterState.list.push({ url: nextUrl, article: art, blockEl: block }); scheduleReaderContentClean(overlay); if (art.nav && art.nav.next) { preloadNextChapter(art.nav.next.href); } } catch (e) { // console.error('[阅读模式] 拼接下一章失败:', e); } finally { try { loadingHint.remove(); } catch(e) {} chapterState.isAppending = false; } } async function jumpToChapter(url) { if (chapterState.isJumping) return; chapterState.isJumping = true; while (contentArea.firstChild) contentArea.removeChild(contentArea.firstChild); chapterState.list = []; const loadingHint = document.createElement('div'); loadingHint.className = 'reader-jump-loading'; loadingHint.textContent = '正在加载...'; const hintColors = getNowColors(); loadingHint.style.cssText = `text-align:center; padding:60px 0; color:${hintColors.subtleTextColor}; font-size:14px;`; contentArea.appendChild(loadingHint); try { const key = normalizeUrl(url); const cached = chapterCache.get(key); let art = null; if (cached && cached.article) { chapterCache.delete(key); art = cached.article; } else { art = await fetchChapterArticle(url); } if (!overlay.isConnected) return; try { loadingHint.remove(); } catch(e) {} if (!art) { notify('章节加载失败,跳转到新页面'); setTimeout(() => { window.location.href = url; }, 800); return; } const block = createChapterBlock(art, url); contentArea.appendChild(block); chapterState.list = [{ url, article: art, blockEl: block }]; chapterState.currentUrl = url; overlay.__currentUrl = url; overlay.__nav = art.nav; refreshImmersiveNavButtons(overlay, art.nav); const immersiveTitle = overlay.querySelector('#reader-immersive-top .rt-title'); if (immersiveTitle) immersiveTitle.textContent = art.title || ''; try { history.pushState({}, '', addReaderModeHash(url)); } catch(e) {} overlay.scrollTo(0, 0); updateProgressBar(overlay, url); scheduleReaderContentClean(overlay); if (art.nav && art.nav.next) { preloadNextChapter(art.nav.next.href); } } catch (e) { // console.error('[阅读模式] 跳转章节失败:', e); try { loadingHint.remove(); } catch(e2) {} } finally { chapterState.isJumping = false; } } function scrollToBlock(block) { try { const overlayRect = overlay.getBoundingClientRect(); const blockRect = block.getBoundingClientRect(); const targetTop = overlay.scrollTop + (blockRect.top - overlayRect.top); const distance = Math.abs(blockRect.top - overlayRect.top); const useSmooth = distance < overlay.clientHeight * 1.5; overlay.scrollTo({ top: targetTop, behavior: useSmooth ? 'smooth' : 'auto' }); } catch(e) { try { block.scrollIntoView(); } catch(e2) {} } } function navigateToChapter(url) { const key = normalizeUrl(url); const blocks = contentArea.querySelectorAll('.reader-chapter-block'); for (const b of blocks) { if (normalizeUrl(b.dataset.url) === key) { scrollToBlock(b); return; } } jumpToChapter(url); } overlay.__onChapterNav = navigateToChapter; const onCatalogClick = (url) => { showCatalog(url, overlay, navigateToChapter); }; 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; contentArea.querySelectorAll('.reader-chapter-title').forEach(el => { el.style.color = colors.textColor; }); contentArea.querySelectorAll('.reader-chapter-content').forEach(el => { el.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 settingsPanel = overlay.querySelector('#reader-settings-view'); if (settingsPanel && typeof settingsPanel.__refreshColors === 'function') settingsPanel.__refreshColors(); }; 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; contentArea.querySelectorAll('.reader-chapter-content').forEach(el => { el.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); }; const firstBlock = createChapterBlock(article, window.location.href); contentArea.appendChild(firstBlock); chapterState.list = [{ url: window.location.href, article, blockEl: firstBlock }]; chapterState.currentUrl = window.location.href; mainContainer.appendChild(contentArea); overlay.appendChild(mainContainer); const immersiveTop = createImmersiveTopBar(article.title, initialColors, onExit, 'edge'); const immersiveBottom = createImmersiveBottomBar( initialColors, onCatalogClick, () => showSettingsPanel(overlay, { onBgChange: handleBgChange, onFontSizeChange: handleFontSizeChange }), handleNightModeClick, getThemeMode, article.nav, 'edge', () => overlay.__nav, navigateToChapter, getAccentForClick ); immersiveBottom.__getRestoreColors = getRestoreColors; overlay.appendChild(immersiveTop); overlay.appendChild(immersiveBottom); overlay.addEventListener('reader-catalog-ready', () => { updateProgressBar(overlay); }); updateProgressBar(overlay); function updateCurrentChapter() { const blocks = contentArea.querySelectorAll('.reader-chapter-block'); if (!blocks.length) return; const overlayRect = overlay.getBoundingClientRect(); const checkY = overlayRect.top + overlay.clientHeight * 0.3; let currentBlock = null; for (const b of blocks) { const rect = b.getBoundingClientRect(); if (rect.top <= checkY) currentBlock = b; else break; } if (!currentBlock) currentBlock = blocks[0]; const url = currentBlock.dataset.url; if (chapterState.currentUrl !== url) { chapterState.currentUrl = url; overlay.__currentUrl = url; const ch = chapterState.list.find(c => c.url === url); if (ch && ch.article) { overlay.__nav = ch.article.nav; refreshImmersiveNavButtons(overlay, ch.article.nav); const immersiveTitle = overlay.querySelector('#reader-immersive-top .rt-title'); if (immersiveTitle) immersiveTitle.textContent = ch.article.title || ''; } updateProgressBar(overlay, url); } } function checkLoadOnScroll() { if (chapterState.isAppending || chapterState.isJumping) return; const last = chapterState.list[chapterState.list.length - 1]; if (!last || !last.article || !last.article.nav || !last.article.nav.next) return; const scrollTop = overlay.scrollTop; const vh = overlay.clientHeight; const totalH = overlay.scrollHeight; if (scrollTop + vh * 2.5 >= totalH) appendNextChapter(); } let immersiveVisible = false, hadSelection = false, lastSelectionActivityTime = 0, selectionPressTime = 0; const markSelectionOnPress = () => { const sel = window.getSelection(); if (sel && sel.toString().trim().length > 0) selectionPressTime = Date.now(); }; overlay.addEventListener('pointerdown', markSelectionOnPress, true); overlay.addEventListener('touchstart', markSelectionOnPress, true); overlay.addEventListener('mousedown', markSelectionOnPress, true); const handleSelectionChange = () => { if (!overlay.isConnected) { document.removeEventListener('selectionchange', handleSelectionChange); return; } const sel = window.getSelection(); if (!sel) return; const text = (sel.toString() || '').trim(); const anchorInOverlay = sel.anchorNode && (overlay === sel.anchorNode || overlay.contains(sel.anchorNode)); if (text.length > 0 && anchorInOverlay) { hadSelection = true; lastSelectionActivityTime = Date.now(); if (immersiveVisible) { immersiveVisible = false; toggleImmersiveVisibility(overlay, false); } } else if (hadSelection) { hadSelection = false; lastSelectionActivityTime = Date.now(); } }; document.addEventListener('selectionchange', handleSelectionChange); overlay.addEventListener('click', (e) => { if (Date.now() - selectionPressTime < 500) return; if (Date.now() - lastSelectionActivityTime < 2000) return; 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')) return; immersiveVisible = !immersiveVisible; toggleImmersiveVisibility(overlay, immersiveVisible); }); let scrollThrottleTimer = null; overlay.addEventListener('scroll', () => { if (immersiveVisible) { immersiveVisible = false; toggleImmersiveVisibility(overlay, false); } updateCurrentChapter(); if (scrollThrottleTimer) return; scrollThrottleTimer = setTimeout(() => { scrollThrottleTimer = null; checkLoadOnScroll(); }, 180); }, { passive: true }); shadow.appendChild(overlay); overlay.focus(); if (article.nav && article.nav.next) { preloadNextChapter(article.nav.next.href); } if (article.nav && article.nav.index) { setTimeout(() => { preloadCatalog(article.nav.index.href, overlay); }, 200); } scheduleReaderContentClean(overlay); return overlay; } function buildOverlayReaderUI(article, initialBgColor = '#ffffff', initialFontSize = '18px') { return buildReaderCore(article, initialBgColor, initialFontSize, exitLegacyReaderMode, 'reader-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 applyFloatingButtonSetting(on) { if (on) { if (!mainButton) createMainButtonOnce(); else { clearButtonTimer(); mainButton.style.display = ''; mainButton.style.opacity = '0.9'; buttonHideTimeout = setTimeout(() => hideButton(mainButton), 3000); } } else { clearButtonTimer(); if (mainButton && mainButton.parentNode) mainButton.parentNode.removeChild(mainButton); mainButton = null; } } function enterLegacyReaderMode(preArticle) { if (isReaderMode) return; hideMainButtonPermanently(); const proceed = () => { const article = preArticle || 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; try { history.replaceState({}, '', addReaderModeHash(window.location.href)); } catch(e) {} updateMenuCommands(); }; if (preArticle) proceed(); else setTimeout(proceed, 50); } function exitLegacyReaderMode() { if (!isReaderMode || !readerOverlay) return; const immersiveBottom = readerOverlay.querySelector('#reader-immersive-bottom'); if (immersiveBottom && typeof immersiveBottom.__cleanup === 'function') immersiveBottom.__cleanup(); if (readerOverlay.__cleanTimers) { readerOverlay.__cleanTimers.forEach(t => clearTimeout(t)); readerOverlay.__cleanTimers = null; } 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; try { history.replaceState({}, '', stripReaderModeHash(originalUrl || window.location.href)); } catch(e) {} updateMenuCommands(); } function updateMenuCommands() { menuCommandIds.forEach(id => { try { GM_unregisterMenuCommand(id); } catch(e) {} }); menuCommandIds = []; if (isReaderMode) { menuCommandIds.push(GM_registerMenuCommand('❌ 退出阅读模式', () => exitLegacyReaderMode())); } else { menuCommandIds.push(GM_registerMenuCommand('打开阅读模式', () => enterLegacyReaderMode())); const floatOn = gmGet(STORAGE_KEYS.FLOATING_BUTTON, DEFAULT_SETTINGS.floatingButton) === true; menuCommandIds.push(GM_registerMenuCommand(floatOn ? '✅ 显示悬浮按钮' : '⬜ 显示悬浮按钮', () => { const next = !(gmGet(STORAGE_KEYS.FLOATING_BUTTON, DEFAULT_SETTINGS.floatingButton) === true); gmSet(STORAGE_KEYS.FLOATING_BUTTON, next); if (!next) { applyFloatingButtonSetting(false); } else { let article = null; try { article = extractContent(); } catch(e) {} if (article && article.fromSelector) applyFloatingButtonSetting(true); } updateMenuCommands(); })); } } function init() { if (SHOULD_AUTO_ACTIVATE) { let attempts = 0; const maxAttempts = 5; const tryExtract = () => { const article = extractContent(); if (article) enterLegacyReaderMode(article); else { attempts++; if (attempts < maxAttempts) setTimeout(tryExtract, 300); } }; tryExtract(); } else { let retryCount = 0; const maxRetries = 5; const intervalId = setInterval(() => { retryCount++; let article = null; if (!isReaderMode) { try { article = extractContent(); } catch(e) {} } if (article && article.fromSelector) { clearInterval(intervalId); if (gmGet(STORAGE_KEYS.FLOATING_BUTTON, DEFAULT_SETTINGS.floatingButton) === true) createMainButtonOnce(); updateMenuCommands(); return; } if (retryCount >= maxRetries) { clearInterval(intervalId); updateMenuCommands(); } }, 300); } } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); })();