// ==UserScript== // @name X阅读模式 // @namespace https://scriptcat.org/scripts/code/7614/X阅读模式.user.js // @version 1.3.4 // @description 小说漫画阅读增强工具:自动进入纯净阅读模式,滚动加载下一章,支持目录选章、字号/行距/主题调节、点击翻页。支持批量下载章节为TXT,分页失败自动重试并备注,停止后已下载章节完整保留。当前网站自动加入白名单并拦截无关跳转,自动保存阅读进度,支持净化词过滤正文,油猴菜单可一键导出诊断日志。 // @author 失辛向南 // @license MIT // @match *://*/* // @run-at document-end // @noframes // @require https://cdnjs.cloudflare.com/ajax/libs/readability/0.4.1/Readability.min.js // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @connect * // ==/UserScript== (function () { 'use strict'; if (window.top !== window.self) return; /* ==================== 诊断日志系统 ==================== */ var __diag = (function () { var MAX = 800; var lines = []; var scriptName = 'X阅读模式'; var version = '1.3.4'; function nowStr() { try { return new Date().toLocaleString('zh-CN', { hour12: false }); } catch (e) { return new Date().toString(); } } function safeStr(o) { try { if (o instanceof Error) return o.name + ': ' + o.message + (o.stack ? '\n' + o.stack.split('\n').slice(0, 6).join('\n') : ''); if (typeof o === 'string') return o; return JSON.stringify(o); } catch (e) { return String(o); } } function push(level, msg) { try { var s = safeStr(msg); if (s.length > 600) s = s.slice(0, 600) + '…'; lines.push('[' + nowStr() + '][' + level + '] ' + s); if (lines.length > MAX) lines.shift(); } catch (e) {} } function wrapConsole() { ['log', 'info', 'warn', 'error', 'debug'].forEach(function (m) { var orig = console[m]; if (typeof orig !== 'function') return; try { console[m] = function () { try { var args = Array.prototype.slice.call(arguments); var text = args.map(function (a) { return typeof a === 'string' ? a : safeStr(a); }).join(' '); if (m === 'error' || m === 'warn') push(m.toUpperCase(), text); else if (text.charAt(0) === '[') push(m.toUpperCase(), text); } catch (e) {} return orig.apply(console, arguments); }; } catch (e) {} }); } function hookErrors() { window.addEventListener('error', function (ev) { try { // 资源加载失败(img/script/link 等):单独记录具体资源 URL var t = ev.target; if (t && t.tagName && !(ev.message)) { var src = t.src || t.currentSrc || t.href || ''; push('ERROR', '资源加载失败: <' + String(t.tagName).toLowerCase() + '> ' + (src || '(无URL)')); return; } var msg = ev.message || ''; var loc = ((ev.filename || '') + ':' + (ev.lineno || '') + ':' + (ev.colno || '')).replace(/^::/, ''); if (ev.error && ev.error.message) msg = ev.error.message; // 过滤无 message、无位置、无 error 对象的空错误事件(多为扩展/无关资源注入,无排障价值) if (!msg && !loc && !ev.error) return; var extra = ''; if (ev.error && ev.error.stack) { extra = '\n' + ev.error.stack.split('\n').slice(0, 5).join('\n'); } push('ERROR', 'window.onerror: ' + msg + ' @ ' + loc + extra); } catch (e2) {} }, true); window.addEventListener('unhandledrejection', function (ev) { var r = ev.reason; push('ERROR', 'unhandledrejection: ' + (r && r.message ? r.message : safeStr(r))); }); } function buildReport() { var out = []; out.push('========== ' + scriptName + ' 诊断日志 =========='); out.push('版本: ' + version); out.push('生成时间: ' + nowStr()); out.push('页面: ' + location.href); out.push('UA: ' + navigator.userAgent); out.push('视口: ' + (window.innerWidth || '') + 'x' + (window.innerHeight || '')); try { out.push('白名单数量: ' + getWhitelist().length); out.push('历史记录数量: ' + getHistory().length); var st = getSettings(); out.push('设置: ' + (st ? safeStr(st) : '(默认)')); } catch (e) { out.push('存储状态读取失败: ' + safeStr(e)); } out.push('-------- 日志记录 (' + lines.length + ' 条) --------'); for (var i = 0; i < lines.length; i++) out.push(lines[i]); return out.join('\n'); } function download() { try { var text = buildReport(); var blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; var d = new Date(), pad = function (n) { return n < 10 ? '0' + n : '' + n; }; a.download = scriptName + '-诊断日志-' + d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + '-' + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds()) + '.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function () { try { URL.revokeObjectURL(url); } catch (e2) {} }, 5000); return true; } catch (e) { try { prompt('无法自动下载日志,请复制以下内容:', buildReport()); } catch (e2) {} return false; } } wrapConsole(); hookErrors(); return { push: push, download: download, buildReport: buildReport, info: function (m) { push('INFO', m); }, warn: function (m) { push('WARN', m); }, error: function (m) { push('ERROR', m); } }; })(); __diag.info('脚本已加载,版本 ' + '1.3.1'); /* ==================== 常量与状态 ==================== */ var KEY_SETTINGS = 'novel_reader_settings_v1'; var KEY_WHITELIST = 'novel_reader_whitelist_v1'; var KEY_HISTORY = 'novel_reader_history_v1'; var KEY_PURIFY = 'novel_reader_purify_v1'; var MAX_STITCH = 500; var state = { open: false, opening: false, root: null, win: null, visited: {}, stitchCount: 0, pendingInit: null, bookKey: null, bookTitle: null, exiting: false // 新增:退出标志,用于 beforeunload 判断 }; var _tocStop = false; /* ==================== 存储 ==================== */ function getSettings() { try { var v = GM_getValue(KEY_SETTINGS, ''); var o = v ? JSON.parse(v) : null; return (o && typeof o === 'object') ? o : null; } catch (e) { return null; } } function getWhitelist() { try { var raw = GM_getValue(KEY_WHITELIST, '[]'); var list = JSON.parse(raw); if (Array.isArray(list) && list.length > 0 && typeof list[0] === 'string') { var newList = list.map(function(d) { return { domain: d, title: '' }; }); saveWhitelist(newList); return newList; } if (Array.isArray(list)) { list = list.filter(function(item) { return item && typeof item === 'object' && item.domain; }); return list; } return []; } catch (e) { return []; } } function saveWhitelist(list) { try { GM_setValue(KEY_WHITELIST, JSON.stringify(list)); } catch (e) {} } function inWhitelist(host) { var list = getWhitelist(); for (var i = 0; i < list.length; i++) { if (list[i].domain === host) return true; } return false; } function getHistory() { try { var v = GM_getValue(KEY_HISTORY, '[]'); var h = JSON.parse(v); return Array.isArray(h) ? h : []; } catch (e) { return []; } } function saveHistory(history) { try { GM_setValue(KEY_HISTORY, JSON.stringify(history)); } catch (e) {} } function getPurifyWords() { try { var v = GM_getValue(KEY_PURIFY, '[]'); var list = JSON.parse(v); if (!Array.isArray(list)) return []; return list.filter(function (w) { return typeof w === 'string' && w.trim(); }); } catch (e) { return []; } } function savePurifyWords(list) { try { GM_setValue(KEY_PURIFY, JSON.stringify(list)); } catch (e) {} } function extractNovelTitle(title) { if (!title) return ''; var t = title.replace(/[((][^))]*[))]/g, '').trim(); var match = t.match(/^(.*?)\s*第[0-9零一二三四五六七八九十百千万亿两]+[章节回卷话篇集]/); if (match) { return match[1].trim() || t; } return t || '未命名'; } function getBookRootKey(url) { try { var u = new URL(url); var path = u.pathname; if (path.endsWith('/')) return u.origin + path; var parts = path.split('/').filter(function(p) { return p; }); if (parts.length === 0) return u.origin + '/'; var last = parts[parts.length - 1]; if (/^\d+$/.test(last) || /^\d+\.html?$/i.test(last) || (/^[a-z0-9_-]+\.html?$/i.test(last) && /\d/.test(last))) { parts.pop(); } var newPath = '/' + parts.join('/'); if (!newPath.endsWith('/')) newPath += '/'; return u.origin + newPath; } catch(e) { return url; } } function addHistory(chapterTitle, url, bookKey, bookTitle) { if (!chapterTitle || !url) return; var cleanUrl = url.split('#')[0]; var key = bookKey || getBookRootKey(cleanUrl); var list = getHistory(); var found = false; var finalTitle = bookTitle || extractNovelTitle(chapterTitle) || '未命名'; for (var i = 0; i < list.length; i++) { var item = list[i]; var existingKey = item.bookKey || getBookRootKey(item.url); if (existingKey === key) { item.timestamp = Date.now(); item.url = cleanUrl; if (!item.bookKey) item.bookKey = key; if (!item.title || item.title === '未命名') { item.title = finalTitle; } found = true; break; } } if (!found) { list.push({ title: finalTitle, url: cleanUrl, timestamp: Date.now(), bookKey: key }); if (list.length > 100) { list.sort(function(a,b) { return a.timestamp - b.timestamp; }); list.shift(); } } saveHistory(list); } function fetchSiteTitle(domain, callback) { if (!domain) { callback(''); return; } var url = 'https://' + domain; GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 5000, onload: function(res) { try { var html = res.responseText; var match = html.match(/]*>([^<]*)<\/title>/i); if (match && match[1]) { callback(match[1].trim()); } else { callback(''); } } catch(e) { callback(''); } }, onerror: function() { callback(''); }, ontimeout: function() { callback(''); } }); } /* ==================== 工具函数 ==================== */ var CHAP_RE = /(第\s*[0-9零一二三四五六七八九十百千两〇○]+\s*[章节節回卷話篇集]|chapter\s*\d+|序章|楔子|尾声|后记|番外)/i; var NAV_WORDS = /^(首页|书架|排行|分类|搜索|登录|注册|完本|最新|书库|书单|我的|消息|反馈|下载|客户端|繁體|繁体|english|返回顶部|上一页|下一页|上一章|下一章|刷新|加入书签|投推荐票|章节错误|字号|夜间模式|手机阅读|电脑版)$/i; var TOC_TEXT_RE = /^(目录|目錄|章节目录|返回目录|查看目录|返回书页|书页|目次|章节列表|列表|目录页)$/; function resolveUrl(href, base) { try { return new URL(href, base).href; } catch (e) { return ''; } } function isBadHref(h2) { return !h2 || h2.indexOf('javascript:') === 0 || h2.charAt(0) === '#' || h2.indexOf('about:') === 0 || h2.indexOf('mailto:') === 0; } function noHash(u) { try { return u.split('#')[0]; } catch (e) { return u; } } function cleanText(t) { return (t || '').replace(/\s+/g, '').trim(); } // 判断 URL 是否为“章节正文页”:末段形如 123.html / 123_1.html / abc123.html。 // 目录页通常以 / 结尾或无 .html 后缀,不会命中。用于宽松目录匹配的护栏。 function looksLikeChapterUrl(u) { try { var segs = (new URL(u).pathname || '').split('/').filter(function (s) { return s; }); var last = segs[segs.length - 1] || ''; return /^\d+(?:_\d+)?\.html?$/i.test(last) || (/^[a-z0-9_-]+\.html?$/i.test(last) && /\d/.test(last)); } catch (e) { return false; } } function chineseToNumber(ch) { if (!ch) return 0; var map = { '零':0,'〇':0,'○':0,'一':1,'二':2,'两':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9,'十':10,'百':100,'千':1000,'万':10000,'亿':100000000 }; // 无位权数字的“逐位序列”写法:一一零=110、一零二=102(部分站点这样写章号) var hasWei = false; for (var w = 0; w < ch.length; w++) { if (map[ch[w]] >= 10) { hasWei = true; break; } } if (!hasWei) { var n = 0; for (var d = 0; d < ch.length; d++) { var dv = map[ch[d]]; if (dv === undefined || dv >= 10) continue; n = n * 10 + dv; } return n; } var num = 0, temp = 0; for (var i=0; i= 10) { if (temp === 0) temp = 1; num += temp * val; temp = 0; } else { temp = val; } } return num + temp; } function romanToNumber(roman) { var map = { 'I':1,'V':5,'X':10,'L':50,'C':100,'D':500,'M':1000 }; var total = 0, prev = 0; for (var i=roman.length-1; i>=0; i--) { var cur = map[roman[i]]; if (!cur) continue; if (cur < prev) total -= cur; else total += cur; prev = cur; } return total; } function parseChapterNumber(title) { if (!title) return null; var original = String(title).trim(); // 预处理①:去掉所有空格(含全角空格),保证空格不影响章号识别 var clean = original.replace(/[\s\u3000]+/g, ''); var n = pickChapterNumber(clean); if (n !== null) return n; // 预处理②:去掉括号及其内容(()、()、【】、〔〕、「」、『』),避免"(第一卷终)"等干扰排序 n = pickChapterNumber(clean.replace(/[((【\[〔〈「『].*?[))】\]〕〉」』]/g, '')); if (n !== null) return n; // 兜底:空格分隔的"12 正文"形式在去空格后无法识别,用原标题再试一次 return pickChapterNumber(original); } function pickChapterNumber(clean) { var match; // 阿拉伯数字章号优先("第405章(第三卷终)"应取章号405,不被括号里的卷号干扰) match = clean.match(/第\s*(\d+)\s*[章节回话篇集]/i); if (match) return parseInt(match[1], 10); // 中文章号(不含"卷":卷是分卷编号,不是章节号,避免"第025章(第一卷终)"取成卷号1) match = clean.match(/第\s*([〇○零一二三四五六七八九十百千万亿两](?:\s*[〇○零一二三四五六七八九十百千万亿两])*)\s*[章节回话篇集]/i); if (match) return chineseToNumber(match[1]); match = clean.match(/chapter\s*(\d+)/i); if (match) return parseInt(match[1], 10); match = clean.match(/chapter\s*([IVXLCDM]+)/i); if (match) return romanToNumber(match[1]); match = clean.match(/^(\d+)[、..\s]/); if (match) return parseInt(match[1], 10); match = clean.match(/^([零一二三四五六七八九十百千万亿两]+)[、..\s]/); if (match) return chineseToNumber(match[1]); match = clean.match(/^([IVXLCDM]+)[、..\s]/); if (match) return romanToNumber(match[1]); return null; } function scoreNext(t) { // 支持 下一章 及变体:下—章、下-章、下–章 if (/^(下一[章節节回卷篇]|下一章节|next\s*chapter|nextchapter|下[—\-–]?[章節节回卷篇])$/i.test(t)) return 5; if (/(下一[章節节回卷篇]|下[—\-–]?[章節节回卷篇])/.test(t)) return 4; // 支持 下一页 及变体:下—页、下-页、下–页 if (/^(下一[页頁張张]|下[—\-–]?[页頁張张]|下页|下節|next\s*page|nextpage)$/i.test(t)) return 2; if (/^(next|›|»|→|>>?)$/i.test(t)) return 1; return 0; } function scorePrev(t) { if (/^(上一[章節节回卷篇]|上一章节|prev\s*chapter|prevchapter)$/i.test(t)) return 5; if (/(上一[章節节回卷篇])/.test(t)) return 4; if (/^(上一[页頁張张]|上页|prev\s*page|prevpage|previous)$/i.test(t)) return 2; if (/^(prev|‹|«|←|< .main > a[href]', must: ['.pagee > .main > a[href]', '.novelinfvie'] }, { name: 'T2', prev: '.read_bg .read_nav a#prev_url[href]', next: '.read_bg .read_nav a#next_url[href]', chapter: '.read_bg .read_nav a#info_url[href]' }, { name: 'T3', prev: '#pagewrap .nav2 .prev a', next: '#pagewrap .nav2 .next a', chapter: '#pagewrap .bcrumb a[rel="category tag"]' }, { name: 'T4', prev: 'div.container > section.RBGsectionTwo > ul > li.RBGsectionTwo-left > a', next: 'div.container > section.RBGsectionTwo > ul > li.RBGsectionTwo-right > a', chapter: 'div.container > section.RBGsectionTwo > ul > li:nth-child(2) > a' }, { name: 'T5', prev: '.article > .next_pre > p:nth-child(1) > a', next: '.article > .next_pre > p:nth-child(2) > a', chapter: '' }, { name: 'T6', prev: '.main > .entry-text > table:nth-child(1) > tbody > tr > td:nth-child(1) > a', next: '.main > .entry-text > table:nth-child(1) > tbody > tr > td:nth-child(2) > a', chapter: '#bcrumb a[rel~="category"]' }, { name: 'T7', prev: '.container > .post > .page > ul > li:nth-child(1) a', next: '.container > .post > .page > ul > li:nth-child(3) a', chapter: '.container > .breadcrumb > ul > li:nth-child(2) > a' }, { name: 'T8', prev: '.chapter-page-btn #pb_prev', next: '.chapter-page-btn #pb_next', chapter: '' }, { name: 'T9', prev: '', next: '', chapter: '#novelbody > .page_chapter > ul > li > a.p3' }, { name: 'T10', prev: '', next: '', chapter: 'table.title > tbody > tr > td:nth-child(1) > a', must: ['.read_content', '.chapter_content'] }, { name: 'T11', prev: '#prev_url', next: '#next_url', chapter: '#info_url', must: ['#ReadSet', '.jumbotron', '#chaptercontent', '.container', '.readpage'] }, { name: 'T12', prev: '#area_newsDet .prevNextBox .pagePrev a', next: '#area_newsDet .prevNextBox .pageNext a', chapter: '#area_newsDet .area_subDetTitle .center a' }, ]; /* ==================== 漫画检测与提取 ==================== */ function isComicPage(doc) { // 已按要求移除漫画网页适配:任何页面都不再判定为漫画,自动加载与内容提取全部走正文(文本)路径。 // 漫画选择器/图片检测全部停用,避免误伤正常小说站点。 return false; } function extractComicImages(doc, baseUrl) { var imgs = []; var seen = {}; var containers = doc.querySelectorAll( '.chapter-content, .comic-content, #images, .chapter-images, .comic-pages, ' + '#manga-container, .comic-viewer, .manga-pages, .comic-panel, .img-content, .page-img, .comic-list, ' + '.comic-main, .reader-main, .comic-reader' ); var targetNodes = containers.length ? containers : [doc.body]; targetNodes.forEach(function (container) { if (!container) return; var allImgs = container.querySelectorAll('img'); allImgs.forEach(function (img) { var src = img.getAttribute('src') || img.getAttribute('data-src') || img.getAttribute('data-original') || img.getAttribute('data-lazy-src') || img.getAttribute('data-url'); if (!src) return; var lowerSrc = src.toLowerCase(); if (/logo|avatar|icon|button|header|footer|bg|ad|banner|loading|thumb/i.test(lowerSrc)) return; var w = img.width || img.naturalWidth || 0; var h = img.height || img.naturalHeight || 0; if (w > 0 && h > 0 && (w < 30 || h < 30)) return; var absUrl = resolveUrl(src, baseUrl); if (!absUrl) return; if (seen[absUrl]) return; seen[absUrl] = true; imgs.push({ src: absUrl, alt: img.getAttribute('alt') || '' }); }); }); if (imgs.length === 0) { var allImgs = doc.querySelectorAll('img'); allImgs.forEach(function (img) { var src = img.getAttribute('src') || img.getAttribute('data-src') || img.getAttribute('data-original'); if (!src) return; var lowerSrc = src.toLowerCase(); if (/logo|avatar|icon|button|header|footer|bg|ad|banner|loading|thumb/i.test(lowerSrc)) return; var absUrl = resolveUrl(src, baseUrl); if (absUrl && !seen[absUrl]) { seen[absUrl] = true; imgs.push({ src: absUrl, alt: img.getAttribute('alt') || '' }); } }); } return imgs.slice(0, 50); } /* ==================== 核心提取函数 ==================== */ function stripTags(html) { return (html || '').replace(/<[^>]*>/g, ''); } function readabilityParse(doc, opts) { try { var article = new Readability(doc.cloneNode(true), opts || {}).parse(); return article || null; } catch (e) { return null; } } function collectRuleContent(doc) { var contentEls = doc.querySelectorAll('p, div.content, div.chapter-content, #chaptercontent, .novel-content'); var texts = []; for (var j = 0; j < contentEls.length; j++) { var text = contentEls[j].innerHTML; if (text && text.length > 100) texts.push(text); } return texts.join('
'); } function ruleHref(doc, baseUrl, sel) { if (!sel) return ''; var a = doc.querySelector(sel); return a ? resolveUrl(a.getAttribute('href'), baseUrl) : ''; } function extractFromDoc(doc, baseUrl) { try { return extractFromDocInner(doc, baseUrl); } catch (e) { // 真实浏览器下个别 DOM 操作可能抛 Illegal invocation 等异常:绝不因此中断,返回空结构让调用方走字符串兜底 try { console.warn('[阅读模式] 内容提取异常:', baseUrl, e && e.message); } catch (e2) {} return { url: baseUrl, title: (doc && doc.title) || '', data: '', textContent: '', next_url: '', prev_url: '', tocUrl: '', toc: [], isComic: false, images: [] }; } } function extractFromDocInner(doc, baseUrl) { var matchedRule = null; for (var i = 0; i < SITE_RULES.length; i++) { var rule = SITE_RULES[i]; if (rule.must && rule.must.every(function (sel) { return doc.querySelector(sel); })) { matchedRule = rule; break; } } var content = ''; var title = doc.title || ''; var nextUrl = '', prevUrl = '', tocUrl = ''; if (matchedRule) { var titleEl = doc.querySelector('title'); if (titleEl) title = titleEl.textContent.trim(); content = collectRuleContent(doc); if (!content || stripTags(content).trim().length < 150) { matchedRule = null; content = ''; } } /* 内容提取:命中规则用规则选择器,否则回退 Readability / 段落抓取 */ if (!matchedRule) { var article = readabilityParse(doc, { charThreshold: 100 }); if (article) { content = article.content || ''; title = article.title || title; } if (!content || content.replace(/\s/g, '').length < 100) { var paragraphs = doc.querySelectorAll('p'); var pTexts = []; for (var k = 0; k < paragraphs.length; k++) { if (paragraphs[k].innerHTML.length > 50) pTexts.push(paragraphs[k].innerHTML); } if (pTexts.length > 1) content = pTexts.join('
'); else { var bodyText = doc.body.innerHTML; if (bodyText && bodyText.length > 200) content = bodyText; } } } /* ---- 下一章 / 目录链接识别:直接照搬 1234.js 逻辑 ---- · rel=next 链接(a[rel=next] 或 link[rel=next])作为 3 分基线 · 全量扫描链接文本,按 scoreNext 取最高分(兼容“下—章/下—页”破折号写法) · 上一章自动翻页已去掉:不再识别 prev,prev_url 恒为空 · 目录链接:先 TOC_TEXT_RE 精确词表匹配,未命中再走“包含匹配+护栏”宽松兜底 */ function relLink(rel) { try { var el = doc.querySelector('a[rel="' + rel + '"], link[rel="' + rel + '"]'); if (el) { var href = el.getAttribute('href'); if (!isBadHref(href)) { var u = resolveUrl(href, baseUrl); if (u && noHash(u) !== noHash(baseUrl)) return u; } } } catch (e) {} return null; } var anchors = []; try { anchors = Array.prototype.slice.call(doc.querySelectorAll('a[href]')); } catch (e) {} var selfUrl = noHash(baseUrl); var rn = relLink('next'); var nextBest = rn ? { url: rn, score: 3 } : null; anchors.forEach(function (a) { var href = a.getAttribute('href'); if (isBadHref(href)) return; var u = resolveUrl(href, baseUrl); if (!u || noHash(u) === selfUrl) return; var t = cleanText(a.textContent); if (!t) return; var ns = scoreNext(t); if (ns > 0 && (!nextBest || ns > nextBest.score)) nextBest = { url: u, score: ns }; }); nextUrl = nextBest ? nextBest.url : ''; prevUrl = ''; // 兜底:常规扫描未命中时,读取站点脚本里的 nextpage 变量(如 var nextpage = "/book/25365/11.html") if (!nextUrl) { try { var scrs = doc.querySelectorAll('script'); for (var si = 0; si < scrs.length; si++) { var st = scrs[si].textContent || ''; var nm = st.match(/var\s+nextpage\s*=\s*["']([^"']+)["']/); if (nm) { var nu2 = resolveUrl(nm[1], baseUrl); if (nu2 && noHash(nu2) !== selfUrl) { nextUrl = nu2; break; } } } } catch (e) {} } // 第一轮:精确词表匹配(目录/章节目录/书页…),整串等于词表词,最可靠,优先使用。 // 与第二轮共用护栏:链接目标不得是章节正文页(目录链接指向目录页,不可能指向 章节数字.html)。 for (var j = 0; j < anchors.length; j++) { var href2 = anchors[j].getAttribute('href'); if (isBadHref(href2)) continue; if (TOC_TEXT_RE.test(cleanText(anchors[j].textContent))) { var tu = resolveUrl(href2, baseUrl); if (tu && noHash(tu) !== selfUrl && !looksLikeChapterUrl(tu)) { tocUrl = tu; break; } } } // 第二轮:宽松“包含匹配”兜底(处理“书页/目录”这类组合文字)。 // 裸包含会误伤:章节标题可能含“目录”(如《第一百二十四章 目录之争》), // 导航文字也可能带“目录”。因此同时满足三重护栏: // ① 文字短(目录标签一般 ≤12 字);② 不是章节标题(不匹配 CHAP_RE); // ③ href 不是正文页(末段不是章节数字.html 文件)。 if (!tocUrl) { for (var j2 = 0; j2 < anchors.length; j2++) { var a2 = anchors[j2]; var href3 = a2.getAttribute('href'); if (isBadHref(href3)) continue; var t3 = cleanText(a2.textContent); if (!t3 || t3.length > 12) continue; if (!/(目录|目錄|书页|書頁|章节目录|目次)/.test(t3)) continue; if (CHAP_RE.test(t3)) continue; var u3 = resolveUrl(href3, baseUrl); if (!u3 || noHash(u3) === selfUrl) continue; if (looksLikeChapterUrl(u3)) continue; tocUrl = u3; break; } } // 第三层备用兜底:正文页显式/宽松目录链接都没找到时,用「删掉章节号」推导书根目录 URL 作为目录页。 // 注意:推导出的页可能只是“最近更新/部分章节”页,真正的完整目录由 fetchToc // 抓取时的“查看完整目录”跟随逻辑(handleFetchToc 内 fullTocUrl 检测)再跳一层补齐。 if (!tocUrl) { try { var derivedToc = getBookRootKey(baseUrl); var derivedPath = ''; try { derivedPath = new URL(derivedToc).pathname; } catch (e) {} // 只在推导确实“删掉了章节文件段”时启用:排除站点根目录(/)与自身地址 if (derivedToc && derivedPath && derivedPath !== '/' && noHash(derivedToc) !== selfUrl) { tocUrl = derivedToc; try { __diag.info('未找到显式目录链接,使用URL推导兜底: ' + tocUrl); } catch (e) {} } } catch (e) {} } if (!content || content.replace(/\s/g, '').length < 50) { var art = readabilityParse(doc); if (art) { content = art.content || ''; title = art.title || title; } } // 漫画适配已移除:isComic 恒 false,images 恒空数组,统一走正文文本路径 var isComic = false; var images = []; return { url: baseUrl, title: title, data: content, textContent: content ? stripTags(content) : '', next_url: nextUrl, prev_url: prevUrl, tocUrl: tocUrl, toc: [], isComic: isComic, images: images }; } // 纯字符串正文/next 兜底:完全不依赖 DOM(避开真实浏览器里 Illegal invocation 类解析异常), // 用正则抓

段落、var nextpage / rel=next / “下一章”锚点、目录锚点 function fallbackExtractHtml(html, baseUrl) { var title = '', data = '', next = '', toc = ''; try { var t = html.match(/]*>([\s\S]*?)<\/title>/i); if (t) title = t[1].replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); var nv = html.match(/var\s+nextpage\s*=\s*["']([^"']+)["']/i); if (nv && nv[1]) next = resolveUrl(nv[1], baseUrl); if (!next) { var rl = html.match(/]*rel=["']next["'][^>]*>/i); if (rl && rl[0]) { var rh = rl[0].match(/href=["']([^"']+)["']/); if (rh && rh[1]) next = resolveUrl(rh[1], baseUrl); } } if (!next) { var na = html.match(/]*href=["']([^"']+)["'][^>]*>[\s\S]*?下一[章节页][\s\S]*?<\/a>/i) || html.match(/]*>[\s\S]*?下一[章节页][\s\S]*?<\/a>/i); if (na) { var nh = na[0].match(/href=["']([^"']+)["']/); if (nh && nh[1]) next = resolveUrl(nh[1], baseUrl); } } if (next && noHash(next) === noHash(baseUrl)) next = ''; var td = html.match(/]*href=["']([^"']+)["'][^>]*>[\s\S]*?(?:目录|章节目录)[\s\S]*?<\/a>/i); if (td && td[1]) toc = resolveUrl(td[1], baseUrl); var ps = html.match(/]*>([\s\S]*?)<\/p>/gi); if (ps && ps.length) { data = ps.join(''); } else { var body = html.match(/]*>([\s\S]*?)<\/body>/i); if (body && body[1]) data = body[1]; } } catch (e) {} return { url: baseUrl, title: title, data: data, textContent: stripTags(data), next_url: next, prev_url: '', tocUrl: toc, isComic: false, images: [] }; } /* ==================== 工具函数(保留) ==================== */ function showToastIn(doc, id, msg, css, dur) { try { var old = doc.getElementById(id); if (old) old.parentNode.removeChild(old); var t = doc.createElement('div'); t.id = id; t.textContent = msg; t.style.cssText = css; (doc.body || doc.documentElement).appendChild(t); setTimeout(function () { try { t.parentNode.removeChild(t); } catch (e) {} }, dur); } catch (e) {} } function toast(msg) { showToastIn(document, '__novel_reader_toast__', msg, 'position:fixed;top:16%;left:50%;transform:translateX(-50%);z-index:2147483647;background:rgba(0,0,0,.78);color:#fff;font-size:14px;line-height:1.5;padding:10px 18px;border-radius:20px;font-family:sans-serif;pointer-events:none;white-space:nowrap;', 2400); } function fetchUrl(url, cb) { try { return GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'text', timeout: 20000, headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' }, onload: function (r) { cb(null, r.responseText || r.response || ''); }, onerror: function () { cb('网络错误'); }, ontimeout: function () { cb('请求超时'); } }); } catch (e) { cb(String(e)); return null; } } function parseHtml(html, baseUrl) { var doc = new DOMParser().parseFromString(html, 'text/html'); try { var baseEl = document.createElement('base'); baseEl.href = baseUrl; doc.head.appendChild(baseEl); } catch (e) {} return doc; } function htmlToText(html) { if (!html) return ''; try { var doc = new DOMParser().parseFromString('

' + html + '
', 'text/html'); var root = doc.getElementById('__tx_root'); var bad = root.querySelectorAll('script,style,noscript,iframe,object,embed,form,input,button,select,textarea,link,meta,svg'); for (var i = 0; i < bad.length; i++) { try { bad[i].parentNode.removeChild(bad[i]); } catch (e) {} } var out = []; (function walk(node) { var children = node.childNodes; for (var k = 0; k < children.length; k++) { var n = children[k]; if (n.nodeType === 3) { out.push(n.nodeValue || ''); } else if (n.nodeType === 1) { var tag = (n.tagName || '').toLowerCase(); if (tag === 'br') { out.push('\n'); continue; } walk(n); if (/^(p|div|li|h[1-6]|blockquote|section|article|tr|ul|ol|table|pre|figure|figcaption|dd|dt|hr)$/.test(tag)) out.push('\n'); } } })(root); var text = out.join(''); text = text.replace(/\r/g, ''); text = text.replace(/[ \t\u00a0\u3000]+/g, ' '); text = text.replace(/ ?\n ?/g, '\n'); text = text.replace(/\n{3,}/g, '\n\n'); return text.trim(); } catch (e) { return String(html).replace(/<[^>]*>/g, '').trim(); } } function cleanDownloadText(text) { if (!text) return text; var lines = text.split('\n'); var cleaned = []; for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); var isChapterWithPage = /^第[0-9零一二三四五六七八九十百千万亿两]+[章节回卷话篇集]/.test(line) && /[((]第.*?页[))]/.test(line); var isOnlyPage = /^[((]第.*?页[))]$/.test(line) || /^第\s*[((]?\s*\d+\s*\/\s*\d+\s*[))]?\s*页$/.test(line); var isNextPagePrompt = /本章.{0,6}完/.test(line) && /下一页继续阅读/.test(line); if (isChapterWithPage || isOnlyPage || isNextPagePrompt) { continue; } else { cleaned.push(lines[i]); } } return cleaned.join('\n'); } // 去掉每页开头的“章节标题行”(第X章/序章/楔子/番外…), // 避免分页站每页正文都内嵌标题,拼接后连出多个“第一章”。finalize 统一在头部加一次章名。 // 护栏:短行(≤30字)且不含句子标点,避免把“第X章……”开头的正文句误删。 function stripLeadingChapterHeading(text) { if (!text) return text; var lines = String(text).split('\n'); var re = /^(第\s*[0-9零一二三四五六七八九十百千两〇○]+\s*[章节節回卷話篇集]|chapter\s*\d+|序章|楔子|尾声|后记|番外)(\s|[::]|$)/i; var changed = false; while (lines.length) { var line = lines[0].trim(); if (!line) { lines.shift(); continue; } // 跳过开头空行 if (line.length <= 30 && re.test(line) && !/[。!?,;、]/.test(line)) { lines.shift(); changed = true; } else break; } if (!changed) return text; return lines.join('\n').replace(/^\n+/, ''); } /* ==================== 正文净化(净化词拦截) ==================== 规则:正文中包含净化词的"句子"整句清除。 · 句子 = 净化词前后直接连接在一起的连续文字(不含标点)。 · 标点(中英文标点)只作分隔符,不纳入连接范围。 · 夹在英文(英文字母 a-zA-Z 两侧)中间的 / 与 . 视为文字,不切断英文整体; 未夹在英文中间的 / 与 . 仍按标点处理(分隔符)。 · 英文净化词(含 a-zA-Z)前后直接紧邻的 / 与 . 视为文字,一并纳入。 · 隔离:净化词前加 / 表示左侧隔离、后加 / 表示右侧隔离—— 隔离侧只清除净化词本身,该侧任何前后文字(含句末标点)都不清理。 · 被清除句子紧跟的句末标点顺带清除,避免正文残留孤立标点。 ================================================================== */ function isPunctChar(c) { return ',。!?;:、,.!?;:()()【】《》〈〉「」『』〔〕[]{}“”‘’"\'…—·–-~@#%^&*=+$¥€<>|\\/'.indexOf(c) !== -1; } function isEnglishWord(w) { return /[a-zA-Z]/.test(w); } function isEngLetter(c) { return /[a-zA-Z]/.test(c); } // 解析净化词:剥离首尾 "/" 作为隔离标记,返回核心词与左右隔离标志 function parsePurifyWord(w) { var s = String(w); var isoLeft = false, isoRight = false; while (s.charAt(0) === '/') { isoLeft = true; s = s.slice(1); } while (s.charAt(s.length - 1) === '/') { isoRight = true; s = s.slice(0, s.length - 1); } return { core: s, isoLeft: isoLeft, isoRight: isoRight }; } function markPurifyRun(text, mark, start, L, eng, isoLeft, isoRight) { var n = text.length; var i, j, k, c; for (i = start; i < start + L && i < n; i++) mark[i] = true; // '/' 或 '.' 是否应视为文字:紧邻英文净化词,或夹在英文字母中间 function slashIsText(idx, adjacentToWord) { var ch = text.charAt(idx); if (ch !== '/' && ch !== '.') return false; if (eng && adjacentToWord) return true; if (idx > 0 && idx < n - 1 && isEngLetter(text.charAt(idx - 1)) && isEngLetter(text.charAt(idx + 1))) return true; return false; } if (!isoLeft) { j = start - 1; while (j >= 0) { c = text.charAt(j); if (isPunctChar(c)) { if (slashIsText(j, j === start - 1)) { mark[j] = true; j--; continue; } break; } mark[j] = true; j--; } } if (!isoRight) { k = start + L; while (k < n) { c = text.charAt(k); if (isPunctChar(c)) { if (slashIsText(k, k === start + L)) { mark[k] = true; k++; continue; } break; } mark[k] = true; k++; } // 清除该句紧跟的连续句末标点,避免孤立标点残留 while (k < n && isPunctChar(text.charAt(k))) { mark[k] = true; k++; } } } function purifyText(text, words) { if (!text || !words || !words.length) return text; var n = text.length; var mark = new Array(n); var i; for (i = 0; i < n; i++) mark[i] = false; for (var wi = 0; wi < words.length; wi++) { var w = words[wi]; if (!w) continue; var p = parsePurifyWord(w); var core = p.core; var L = core.length; if (L === 0 || L > n) continue; var eng = isEnglishWord(core); var pos = 0; while (pos <= n - L) { var idx = text.indexOf(core, pos); if (idx === -1) break; markPurifyRun(text, mark, idx, L, eng, p.isoLeft, p.isoRight); pos = idx + 1; } } var out = ''; for (i = 0; i < n; i++) if (!mark[i]) out += text.charAt(i); return out; } // 对已插入 DOM 的元素递归净化其文本节点(用于净化词变更后即时重净) function purifyDomElement(el, words) { if (!el || !words || !words.length) return; var walk = function (node) { var children = node.childNodes; for (var i = 0; i < children.length; i++) { var c = children[i]; if (c.nodeType === 3) { var v = c.nodeValue; if (v && /[A-Za-z0-9\u4e00-\u9fa5]/.test(v)) { var clean = purifyText(v, words); if (clean !== v) c.nodeValue = clean; } } else if (c.nodeType === 1) { walk(c); } } }; walk(el); } // 对 HTML 字符串净化(渲染前处理),保留标签结构,只清除文本节点中的命中句子 function purifyHtml(html, words) { if (!html || !words || !words.length) return html; try { var doc = new DOMParser().parseFromString('
' + html + '
', 'text/html'); var root = doc.getElementById('__purify_root'); if (!root) return html; purifyDomElement(root, words); return root.innerHTML; } catch (e) { return html; } } // 下载文本净化:按行(段落)分别净化,与阅读器逐文本节点净化保持一致 function purifyLines(text, words) { if (!text || !words || !words.length) return text; var lines = String(text).split('\n'); for (var i = 0; i < lines.length; i++) { if (/[A-Za-z0-9\u4e00-\u9fa5]/.test(lines[i])) { var clean = purifyText(lines[i], words); if (clean !== lines[i]) lines[i] = clean; } } return lines.join('\n'); } function saveTextFile(text) { var title = state.bookTitle || window.top.document.title || 'novel'; var blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = title.replace(/[\*\/:<>\?\\\|\r\n,]/g, '_') + '.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function () { try { URL.revokeObjectURL(a.href); } catch (e) {} }, 10000); } /* ==================== iframe 阅读器(与之前一致,包含图片过滤) ==================== */ var READER_CSS = "*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}body,html{height:100%;overflow:hidden}body{font-family:-apple-system,BlinkMacSystemFont,\"PingFang SC\",\"Hiragino Sans GB\",\"Microsoft YaHei\",sans-serif;transition:background-color .3s,color .3s}body.theme-white{background:#fff;color:#2b2b2b}body.theme-warm{background:#f7f0e3;color:#4a3f35}body.theme-green{background:#d7e8d4;color:#364736}body.theme-dark{background:#1b1b1f;color:#a8a8a8}.topbar{position:fixed;top:0;left:0;right:0;z-index:100;height:48px;display:flex;align-items:center;padding:0 12px;transition:transform .25s,background-color .3s;transform:translateY(-100%)}.topbar.visible{transform:translateY(0)}.theme-white .topbar{background:rgba(255,255,255,.96);box-shadow:0 2px 8px rgba(0,0,0,.05)}.theme-warm .topbar{background:rgba(247,240,227,.96);box-shadow:0 2px 8px rgba(0,0,0,.05)}.theme-green .topbar{background:rgba(215,232,212,.96);box-shadow:0 2px 8px rgba(0,0,0,.05)}.theme-dark .topbar{background:rgba(27,27,31,.96);box-shadow:0 2px 8px rgba(0,0,0,.2)}.topbar .btn-back{width:36px;height:36px;border:none;background:0 0;font-size:22px;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:50%}.theme-green .btn-back,.theme-warm .btn-back,.theme-white .btn-back{color:#555}.theme-dark .btn-back{color:#aaa}.topbar .bar-title{flex:1;text-align:center;font-size:15px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 8px;opacity:.7}.topbar .btn-catalog{width:36px;height:36px;border:none;background:0 0;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:50%}.topbar .btn-catalog svg{width:20px;height:20px}.theme-green .btn-catalog,.theme-warm .btn-catalog,.theme-white .btn-catalog{color:#555}.theme-dark .btn-catalog{color:#aaa}.content-wrapper{position:relative;height:100%;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:60px 20px 80px}.chapter-block{margin-bottom:24px}.chapter-divider{display:flex;align-items:center;justify-content:center;gap:8px;min-width:0;margin:16px 0 24px;opacity:.4;font-size:12px;white-space:nowrap}.chapter-divider::after,.chapter-divider::before{content:\"\";display:block;flex:1 1 12px;min-width:12px;max-width:30%;height:1px;background:currentColor;opacity:.5}.chapter-divider-title{display:block;flex:0 1 auto;min-width:0;max-width:calc(100% - 56px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.chapter-title{font-weight:700;text-align:center;margin-bottom:24px;line-height:1.4}.chapter-content{line-height:1.9;text-align:justify;word-break:break-all}.chapter-content p{margin-bottom:1em}.chapter-content img{max-width:100%;height:auto;border-radius:4px;margin:8px 0}.stitch-loading{text-align:center;padding:16px 0;opacity:.5;font-size:13px;visibility:hidden;min-height:48px}.stitch-loading.visible{visibility:visible}.stitch-loading .spinner{display:inline-block;width:14px;height:14px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:spin .8s linear infinite;vertical-align:middle;margin-right:6px}.chapter-content img.reader-img-loading{min-height:160px;background-color:#eee;background-repeat:no-repeat;background-position:center;background-size:auto;background-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='200' viewBox='0 0 300 200'%3E%3Crect width='100%25' height='100%25' fill='%23eeeeee'/%3E%3Cg fill='%23bbbbbb' transform='translate(130,80)'%3E%3Crect x='0' y='0' width='40' height='30' rx='3' ry='3' fill='none' stroke='%23bbbbbb' stroke-width='2'/%3E%3Ccircle cx='10' cy='10' r='3'/%3E%3Cpath d='M2,28 L14,16 L22,22 L32,10 L38,16 L38,28 Z'/%3E%3C/g%3E%3Ctext x='50%25' y='75%25' text-anchor='middle' font-family='sans-serif' font-size='12' fill='%23999999'%3ELoading...%3C/text%3E%3C/svg%3E\")}.loading{display:flex;align-items:center;justify-content:center;height:60%;flex-direction:column;opacity:.5;font-size:14px}.loading .spinner{width:24px;height:24px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:spin .8s linear infinite;margin-bottom:12px}@keyframes spin{to{transform:rotate(360deg)}}.bottombar{position:fixed;bottom:0;left:0;right:0;z-index:100;transition:transform .25s,background-color .3s;transform:translateY(100%)}.bottombar.visible{transform:translateY(0)}.theme-white .bottombar{background:rgba(255,255,255,.97);box-shadow:0 -2px 8px rgba(0,0,0,.05)}.theme-warm .bottombar{background:rgba(247,240,227,.97);box-shadow:0 -2px 8px rgba(0,0,0,.05)}.theme-green .bottombar{background:rgba(215,232,212,.97);box-shadow:0 -2px 8px rgba(0,0,0,.05)}.theme-dark .bottombar{background:rgba(27,27,31,.97);box-shadow:0 -2px 8px rgba(0,0,0,.2)}.toolbar-row{display:flex;height:52px;align-items:center;justify-content:space-around}.toolbar-btn{border:none;background:0 0;font-size:12px;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:3px;padding:4px 6px;border-radius:8px;transition:all .15s}.toolbar-btn svg{width:22px;height:22px}.theme-white .toolbar-btn{color:#555}.theme-warm .toolbar-btn{color:#6b5d4e}.theme-green .toolbar-btn{color:#4a6a4a}.theme-dark .toolbar-btn{color:#999}.toolbar-btn:active{opacity:.6}.toolbar-btn.active{font-weight:600}.theme-white .toolbar-btn.active{color:#333}.theme-warm .toolbar-btn.active{color:#4a3f35}.theme-green .toolbar-btn.active{color:#2d4a2d}.theme-dark .toolbar-btn.active{color:#ddd}.settings-panel{position:fixed;bottom:0;left:0;right:0;z-index:200;padding:20px 20px 28px;border-radius:16px 16px 0 0;transition:transform .3s cubic-bezier(.32,.72,0,1);transform:translateY(100%)}.settings-panel.visible{transform:translateY(0)}.theme-white .settings-panel{background:#fff;box-shadow:0 -4px 20px rgba(0,0,0,.1)}.theme-warm .settings-panel{background:#f7f0e3;box-shadow:0 -4px 20px rgba(0,0,0,.08)}.theme-green .settings-panel{background:#d7e8d4;box-shadow:0 -4px 20px rgba(0,0,0,.08)}.theme-dark .settings-panel{background:#262630;box-shadow:0 -4px 20px rgba(0,0,0,.3)}.settings-panel .panel-handle{width:36px;height:4px;border-radius:2px;margin:0 auto 16px}.theme-white .panel-handle{background:#ddd}.theme-warm .panel-handle{background:#d6c9ae}.theme-green .panel-handle{background:#b0c9ac}.theme-dark .panel-handle{background:#444}.settings-mask{position:fixed;top:0;left:0;right:0;bottom:0;z-index:150;background:rgba(0,0,0,.3);display:none}.settings-mask.visible{display:block}.setting-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.setting-row:last-child{margin-bottom:0}.setting-label{font-size:13px;opacity:.6;min-width:48px}.font-controls{display:flex;align-items:center;flex:1;margin-left:12px}.font-controls button{width:40px;height:36px;border:none;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer;transition:all .15s}.theme-white .font-controls button{background:#f0f0f0;color:#333}.theme-warm .font-controls button{background:#ebe0cc;color:#4a3f35}.theme-green .font-controls button{background:#c1d9bd;color:#364736}.theme-dark .font-controls button{background:#2a2a30;color:#ccc}.font-controls button:active{transform:scale(.92)}.font-controls .font-size-val{flex:1;text-align:center;font-size:14px;font-weight:500}.spacing-controls{display:flex;gap:8px;flex:1;margin-left:12px}.spacing-controls button{flex:1;height:34px;border:none;border-radius:8px;font-size:12px;cursor:pointer;transition:all .15s}.theme-white .spacing-controls button{background:#f0f0f0;color:#555}.theme-warm .spacing-controls button{background:#ebe0cc;color:#5c4b37}.theme-green .spacing-controls button{background:#c1d9bd;color:#364736}.theme-dark .spacing-controls button{background:#2a2a30;color:#999}.spacing-controls button.active{font-weight:700}.theme-white .spacing-controls button.active{background:#333;color:#fff}.theme-warm .spacing-controls button.active{background:#7a6652;color:#f7f0e3}.theme-green .spacing-controls button.active{background:#4a6e4a;color:#e8f4e6}.theme-dark .spacing-controls button.active{background:#555;color:#eee}.theme-options{display:flex;gap:12px;flex:1;margin-left:12px;justify-content:center}.theme-dot{width:36px;height:36px;border-radius:50%;border:2px solid transparent;cursor:pointer;position:relative;transition:all .15s}.theme-dot:active{transform:scale(.9)}.theme-dot.active{border-color:#666}.theme-dark .theme-dot.active{border-color:#aaa}.dot-white{background:#fff;box-shadow:inset 0 0 0 1px #ddd}.dot-warm{background:#f7f0e3;box-shadow:inset 0 0 0 1px #d6c9ae}.dot-green{background:#d7e8d4;box-shadow:inset 0 0 0 1px #a8c9a4}.dot-dark{background:#1b1b1f;box-shadow:inset 0 0 0 1px #444}.overlay{position:fixed;top:0;left:0;right:0;bottom:0;z-index:50;display:none}.overlay.visible{display:block}.catalog-mask{position:fixed;top:0;left:0;right:0;bottom:0;z-index:250;background:rgba(0,0,0,.4);opacity:0;visibility:hidden;transition:opacity .3s,visibility .3s}.catalog-mask.visible{opacity:1;visibility:visible}.catalog-sidebar{position:fixed;top:0;right:0;bottom:0;width:75%;max-width:320px;z-index:300;display:flex;flex-direction:column;transform:translateX(100%);transition:transform .3s cubic-bezier(.32,.72,0,1)}.catalog-sidebar.visible{transform:translateX(0)}.theme-white .catalog-sidebar{background:#fff}.theme-warm .catalog-sidebar{background:#f7f0e3}.theme-green .catalog-sidebar{background:#d7e8d4}.theme-dark .catalog-sidebar{background:#1e1e24}.catalog-header{padding:16px 20px 12px;font-size:16px;font-weight:600;flex-shrink:0;display:flex;align-items:center;justify-content:space-between}.theme-white .catalog-header{border-bottom:1px solid #eee}.theme-warm .catalog-header{border-bottom:1px solid #e2d6c0}.theme-green .catalog-header{border-bottom:1px solid #b8cfb4}.theme-dark .catalog-header{border-bottom:1px solid #333}.catalog-header .header-buttons{display:flex;gap:6px;align-items:center}.catalog-header button{font-size:12px;border:none;background:rgba(128,128,128,.16);color:inherit;opacity:.75;border-radius:12px;padding:4px 10px;cursor:pointer;white-space:nowrap}.catalog-header button:hover{opacity:1}.catalog-list{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:8px 0}.catalog-item{display:block;padding:12px 20px;font-size:14px;line-height:1.4;cursor:pointer;transition:background .15s;text-decoration:none;color:inherit}.catalog-item:active{opacity:.6}.catalog-item.active{font-weight:600}.theme-white .catalog-item.active{color:#1a73e8;background:#f0f6ff}.theme-warm .catalog-item.active{color:#8b6914;background:#f0e8d0}.theme-green .catalog-item.active{color:#2e7d32;background:#c5e1c8}.theme-dark .catalog-item.active{color:#7cacf8;background:#262630}.catalog-empty{padding:40px 20px;text-align:center;font-size:13px;opacity:.4}.panel-extra{position:fixed;bottom:0;left:0;right:0;z-index:220;padding:20px 20px 28px;border-radius:16px 16px 0 0;transition:transform .3s cubic-bezier(.32,.72,0,1);transform:translateY(100%);height:70vh;display:flex;flex-direction:column}.panel-extra.visible{transform:translateY(0)}.theme-white .panel-extra{background:#fff;box-shadow:0 -4px 20px rgba(0,0,0,.1)}.theme-warm .panel-extra{background:#f7f0e3;box-shadow:0 -4px 20px rgba(0,0,0,.08)}.theme-green .panel-extra{background:#d7e8d4;box-shadow:0 -4px 20px rgba(0,0,0,.08)}.theme-dark .panel-extra{background:#262630;box-shadow:0 -4px 20px rgba(0,0,0,.3)}.panel-extra .panel-handle{width:36px;height:4px;border-radius:2px;margin:0 auto 16px}.theme-white .panel-handle{background:#ddd}.theme-warm .panel-handle{background:#d6c9ae}.theme-green .panel-handle{background:#b0c9ac}.theme-dark .panel-handle{background:#444}.panel-extra .panel-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;font-size:16px;font-weight:600}.panel-extra .panel-close{background:0 0;border:none;font-size:22px;cursor:pointer;opacity:.5;padding:0 6px;transition:opacity .2s}.panel-extra .panel-close:hover{opacity:1}.panel-extra .panel-list-wrap{position:relative;flex:1;overflow:hidden;min-height:0}.panel-extra .panel-list{position:absolute;top:0;left:0;right:0;bottom:0;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:4px 0;scroll-behavior:smooth}.panel-extra .panel-item{padding:10px 6px;border-bottom:1px solid rgba(128,128,128,.15);display:flex;justify-content:space-between;align-items:center;font-size:14px;transition:background .1s;cursor:default}.panel-extra .panel-item .item-main{flex:1;display:flex;justify-content:space-between;align-items:center;overflow:hidden;cursor:pointer}.panel-extra .panel-item .item-main:active{opacity:.6}.panel-extra .panel-item .item-title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:8px}.panel-extra .panel-item .item-del{background:0 0;border:none;font-size:16px;cursor:pointer;opacity:.3;padding:0 4px;transition:opacity .2s;color:inherit;line-height:1}.panel-extra .panel-item .item-del:hover{opacity:.9}.panel-extra .panel-empty{padding:30px 20px;text-align:center;opacity:.4;font-size:13px}.panel-extra .fade-mask{position:absolute;left:0;right:0;height:24px;pointer-events:none;z-index:2;transition:opacity .2s}.panel-extra .fade-mask.top{top:0;background:linear-gradient(to bottom,var(--bg),transparent)}.panel-extra .fade-mask.bottom{bottom:0;background:linear-gradient(to top,var(--bg),transparent)}.panel-extra .fade-mask.hidden{opacity:0}.theme-white .panel-extra{--bg:#fff}.theme-warm .panel-extra{--bg:#f7f0e3}.theme-green .panel-extra{--bg:#d7e8d4}.theme-dark .panel-extra{--bg:#262630}"; var READER_BODY = "
\n \n
\n \n
\n
\n
\n
\n
\n 目录\n \n
\n \n \n
\n
\n
暂无目录
\n
\n
\n
正在加载内容...
\n
\n
\n
正在加载下一章...
\n
\n
\n
\n
\n \n \n \n \n \n
\n
\n
\n
\n
\n
字号\n
18
\n
\n
行距\n
\n
\n
背景\n
\n
\n
\n\n
\n
\n
白名单网站
\n
\n
\n
暂无白名单
\n
\n
\n
\n\n
\n
\n
阅读历史
\n
\n
\n
暂无历史记录
\n
\n
\n
\n\n
\n
\n
净化词
\n
\n \n \n
\n
净化说明(阅读和下载TXT都会生效)
1. 整句清除:正文出现净化词,就把它连着的整句删掉——词前面连着的字、词本身、后面连着的字和句末标点(。!?…)全清;前后若遇到标点就停,标点另一侧的字不删。例:删“广告”,句子“本书由广告赞助。”整句消失。
2. 一次加多个词:用隔开即可。例:广告加群扫码。
3. 只想删词、不误删旁边的字:在词前或词后加/。词前加/(如/广告)=左边的字不误删;词后加/(如广告/)=右边的字和句末标点都不误删;两边都加/(如/广告/)=只删“广告”二字,最保险。
4. 英文单词不怕被切开:夹在字母中间的/.算单词的一部分(如 a/b、U.S. 不会被切断),没夹在字母里只当普通标点;净化词本身是英文时,它紧挨着的/.也会一并清除。
5. 删除词:点词条右侧的即可。
\n
\n
\n
暂无净化词
\n
\n
\n
\n
"; /* ==================== iframe 阅读器脚本(含图片过滤和拦截) ==================== */ var SHARED_SRC = [showToastIn, getBookRootKey, chineseToNumber, romanToNumber, parseChapterNumber, pickChapterNumber, isPunctChar, isEnglishWord, isEngLetter, parsePurifyWord, markPurifyRun, purifyText, purifyDomElement, purifyHtml] .map(function (f) { return f.toString(); }).join('\n'); function iframeMain() { var m = document.getElementById('topbar'), h = document.getElementById('bottombar'), e = document.getElementById('overlay'), E = document.getElementById('contentWrapper'), g = document.getElementById('articleContainer'), f = document.getElementById('chaptersHost'), p = document.getElementById('loadingTip'), v = document.getElementById('barTitle'), b = document.getElementById('fontSizeVal'), y = document.getElementById('stitchLoadingNext'), n = document.getElementById('catalogSidebar'), r = document.getElementById('catalogMask'), _ = document.getElementById('catalogList'), btnLoadToc = document.getElementById('btnLoadToc'), btnDownloadAll = document.getElementById('btnDownloadAll'), tocProgress = document.getElementById('tocProgress'), loadingText = document.getElementById('loadingText'); var STR = { readerMode: '阅读模式', chapters: '目录', noChapters: '暂无目录', loading: '正在加载内容...', loadingJump: '正在加载章节...', loadingNext: '正在加载下一章...', nextChapter: '下一章', loadFail: '章节加载失败', tocLoaded: '已加载目录', night: '夜间', day: '日间', download: '下载全部', downloading: '正在下载...' }; document.title = STR.readerMode; loadingText.textContent = STR.loading; var C = [], d = 0; var o = false, G = false; var stitching = false, awaitingNextUrl = '', stitchFails = 0; var tocData = [], tocUrl = ''; var tocLoading = false; var s = { fontSize: 18, lineHeight: 1.9, theme: 'warm' }; var currentBookKey = ''; var currentBookTitle = ''; var currentHost = ''; var isComicMode = false; var selectMode = false, dlRunning = false, selectedUrls = {}; var selectToolbar = null; var markActivePrev = null; var lastScrollTop = 0; var scrollTicking = false; var extraVisible = false; var extraMask = document.getElementById('extraMask'); var purifyWords = []; var whitelistPanel = document.getElementById('whitelistPanel'); var historyPanel = document.getElementById('historyPanel'); var btnNight = document.getElementById('btnNightMode'); var Y = document.getElementById('settingsPanel'), j = document.getElementById('settingsMask'); function toast(msg) { showToastIn(document, '__reader_toast__', msg, 'position:fixed;top:18%;left:50%;transform:translateX(-50%);z-index:999;background:rgba(0,0,0,.72);color:#fff;font-size:13px;line-height:1.5;padding:9px 16px;border-radius:18px;pointer-events:none;', 2200); } // ====================== 图片过滤函数 ====================== function filterImages(container) { var imgs = container.querySelectorAll('img'); imgs.forEach(function(img) { var src = img.getAttribute('src') || img.getAttribute('data-src') || ''; if (/\.gif(\?.*)?$/i.test(src)) { img.remove(); return; } if (img.complete && img.naturalWidth > 0) { if (img.naturalWidth > img.naturalHeight) { img.remove(); } return; } img.addEventListener('load', function() { if (this.naturalWidth > this.naturalHeight) { this.remove(); } }); img.addEventListener('error', function() { this.remove(); }); }); } function getHighlightColor() { switch (s.theme) { case 'white': return '#e6f2ff'; case 'warm': return '#e8ddc8'; case 'green': return '#c5e1c8'; case 'dark': return '#3a4a5a'; default: return '#e6f2ff'; } } function send(msg) { try { window.parent.postMessage(msg, '*'); } catch (err) {} } function sendCurrent(url, title) { send({ type: 'setCurrentUrl', url: url || '', title: title || '', bookKey: currentBookKey, bookTitle: currentBookTitle }); } // ====== 阅读器内部日志转发:把 iframe 内的 console/异常转发给父页面诊断日志 ====== function forwardDiag(level, msg) { try { var text = (typeof msg === 'string') ? msg : (function () { try { return JSON.stringify(msg); } catch (e) { return String(msg); } })(); if (text.length > 600) text = text.slice(0, 600) + '…'; send({ type: '__diag', level: level, msg: text }); } catch (e) {} } try { ['log', 'info', 'warn', 'error', 'debug'].forEach(function (m) { var orig = console[m]; if (typeof orig !== 'function') return; console[m] = function () { try { var args = Array.prototype.slice.call(arguments); var text = args.map(function (a) { return (typeof a === 'string') ? a : (function () { try { return JSON.stringify(a); } catch (e) { return String(a); } })(); }).join(' '); if (m === 'error' || m === 'warn' || text.charAt(0) === '[') forwardDiag(m.toUpperCase(), text); } catch (e) {} return orig.apply(console, arguments); }; }); window.addEventListener('error', function (ev) { forwardDiag('ERROR', 'iframe window.onerror: ' + (ev.message || '') + ' @ ' + (ev.filename || '') + ':' + (ev.lineno || '')); }, true); } catch (e) {} window.addEventListener('message', function (ev) { var msg = ev.data; if (!msg || typeof msg !== 'object' || !msg.type) return; handleMessage(msg); }); function handleMessage(msg) { switch (msg.type) { case 'init': if (msg.settings && typeof msg.settings === 'object') { if (msg.settings.fontSize) s.fontSize = msg.settings.fontSize; if (msg.settings.lineHeight) s.lineHeight = msg.settings.lineHeight; if (msg.settings.theme) s.theme = msg.settings.theme; } currentBookKey = msg.bookKey || ''; currentBookTitle = msg.bookTitle || ''; currentHost = msg.host || ''; isComicMode = !!msg.isComic; if (Array.isArray(msg.purify)) purifyWords = msg.purify; applySettings(); // ====== 新增:内联图片高度限制(仅小说模式) ====== if (!isComicMode) { var style = document.createElement('style'); style.id = 'inline-img-style'; style.textContent = ` .chapter-content p img { max-height: 1.6em; vertical-align: middle; margin: 0 2px; } `; document.head.appendChild(style); } tocData = msg.toc || []; tocUrl = msg.tocUrl || ''; btnLoadToc.style.display = tocUrl ? '' : 'none'; if (msg.chapter) { if (isComicMode && msg.images && msg.images.length) { renderComicImages(msg.images, msg.chapter.title); } else { J(msg.chapter, true); } I(msg.chapter); } updateTocUI(); break; case 'setBookInfo': currentBookKey = msg.bookKey || currentBookKey; currentBookTitle = msg.bookTitle || currentBookTitle; if (typeof updateTocUI === 'function') updateTocUI(); break; case 'chapterReady': y.classList.remove('visible'); if (msg.mode === 'jump') { hideLoading(); if (isComicMode && msg.chapter.images && msg.chapter.images.length) { renderComicImages(msg.chapter.images, msg.chapter.title); } else { resetAndLoad(msg.chapter); } } else { if (awaitingNextUrl && msg.url === awaitingNextUrl) { awaitingNextUrl = ''; stitching = false; if (isComicMode && msg.chapter.images && msg.chapter.images.length) { appendComicImages(msg.chapter.images); } else { J(msg.chapter, false); } stitchFails = 0; setTimeout(checkAutoLoad, 0); } else { try { console.warn('[阅读模式] 自动加载状态异常:等待', awaitingNextUrl || '(空)', '但收到', msg.url || '(空)'); } catch (e1) {} } } break; case 'chapterFailed': y.classList.remove('visible'); if (msg.mode === 'jump') { hideLoading(); toast(STR.loadFail + (msg.reason ? ':' + msg.reason : '')); } else { stitching = false; awaitingNextUrl = ''; stitchFails++; if (stitchFails < 3) setTimeout(checkAutoLoad, 2000); else { try { toast('下一页加载失败:' + (msg.reason || '')); } catch (e1) {} } try { console.warn('[阅读模式] 章节自动加载失败(第' + (stitchFails) + '次):', msg.url || '', msg.reason || ''); } catch (e1) {} } break; case 'tocReady': tocData = msg.toc || []; tocLoading = false; resetTocBtn(); renderCatalog(); updateTocUI(); break; case 'tocProgress': if (tocProgress.style.display !== 'none') { tocProgress.textContent = '已获取 ' + (msg.count || 0) + ' 章'; } break; case 'whitelistData': renderWhitelist(msg.list || []); break; case 'purifyUpdate': purifyWords = Array.isArray(msg.purify) ? msg.purify : []; (function () { var cs = f.querySelectorAll('.chapter-content'); for (var pi = 0; pi < cs.length; pi++) purifyDomElement(cs[pi], purifyWords); })(); break; case 'purifyData': renderPurify(msg.list || []); break; case 'historyData': renderHistory(msg.list || []); break; case 'downloadStopping': break; case 'downloadComplete': resetDlBtn(); toast('下载完成!'); break; case 'downloadPaused': resetDlBtn(); toast('已停止下载,保存了 ' + msg.current + ' 章'); break; case 'downloadProgress': btnDownloadAll.textContent = '下载中 ' + msg.current + '/' + msg.total; break; } } function showContent(title) { v.textContent = title || STR.readerMode; p.style.display = 'none'; g.style.display = 'block'; } function showLoading(text) { loadingText.textContent = text || STR.loading; p.style.display = 'flex'; g.style.display = 'none'; } function hideLoading() { p.style.display = 'none'; g.style.display = 'block'; } function hideBars() { o = false; m.classList.remove('visible'); h.classList.remove('visible'); e.classList.remove('visible'); } function makeComicImg(src, alt) { var imgEl = document.createElement('img'); imgEl.src = src; imgEl.alt = alt; imgEl.style.cssText = 'max-width:100%;height:auto;display:block;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.1);'; return imgEl; } function renderComicImages(images, title) { f.innerHTML = ''; var container = document.createElement('div'); container.className = 'comic-page'; container.style.cssText = 'display:flex;flex-direction:column;align-items:center;gap:16px;'; images.forEach(function (img) { container.appendChild(makeComicImg(img.src, img.alt || title || '漫画')); }); f.appendChild(container); filterImages(container); showContent(title); E.scrollTop = 0; sendCurrent(location.href, title); } function appendComicImages(images) { var container = f.querySelector('.comic-page'); if (!container) { renderComicImages(images, ''); return; } images.forEach(function (img) { container.appendChild(makeComicImg(img.src, img.alt || '')); }); filterImages(container); E.scrollTop = E.scrollHeight; } function sanitize(el) { var bad = el.querySelectorAll('script,iframe,object,embed,link,meta,form,input,button,select,textarea'); for (var i = 0; i < bad.length; i++) { try { bad[i].parentNode.removeChild(bad[i]); } catch (e2) {} } } function J(t, first) { if (!t || !t.url) return; var block = document.createElement('div'); block.className = 'chapter-block'; block.dataset.url = t.url || ''; if (!first) { var divider = document.createElement('div'); divider.className = 'chapter-divider'; var dt = document.createElement('span'); dt.className = 'chapter-divider-title'; dt.textContent = t.title || STR.nextChapter; divider.appendChild(dt); block.appendChild(divider); } var titleEl = document.createElement('div'); titleEl.className = 'chapter-title'; titleEl.textContent = t.title || ''; titleEl.style.fontSize = (s.fontSize + 4) + 'px'; block.appendChild(titleEl); var contentEl = document.createElement('div'); contentEl.className = 'chapter-content'; contentEl.style.fontSize = s.fontSize + 'px'; contentEl.style.lineHeight = s.lineHeight; contentEl.innerHTML = purifyHtml(t.data || '', purifyWords); sanitize(contentEl); filterImages(contentEl); block.appendChild(contentEl); f.appendChild(block); C.push({ url: t.url, title: t.title || '', nextUrl: t.next_url || '', element: block }); var imgs = block.querySelectorAll('img[data-src]'); for (var i = 0; i < imgs.length; i++) { (function (img) { var src = img.getAttribute('data-src'); if (!src) return; var im = new Image(); im.onload = function () { img.src = src; img.removeAttribute('data-src'); }; im.onerror = function () { img.removeAttribute('data-src'); }; im.src = src; })(imgs[i]); } if (first) { d = 0; showContent(t.title); E.scrollTop = 0; sendCurrent(t.url, t.title); } renderCatalog(); } function I(t) { if (t && t.next_url) M(t.url, t.next_url); } function findLoadedByUrl(url) { if (!url) return -1; for (var i = 0; i < C.length; i++) if (C[i].url === url) return i; return -1; } function M(fromUrl, nextUrl) { if (!nextUrl || stitching) return; if (findLoadedByUrl(nextUrl) !== -1) return; stitching = true; awaitingNextUrl = nextUrl; y.classList.add('visible'); send({ type: 'fetchChapter', url: nextUrl, from: fromUrl, mode: 'stitch' }); } function resetAndLoad(chapter) { C = []; d = 0; f.innerHTML = ''; stitching = false; awaitingNextUrl = ''; stitchFails = 0; y.classList.remove('visible'); J(chapter, true); I(chapter); E.scrollTop = 0; } function updateTocUI() { if (tocLoading) { btnDownloadAll.style.display = 'none'; tocProgress.style.display = 'inline'; var count = tocData ? tocData.length : 0; tocProgress.textContent = count > 0 ? '已获取 ' + count + ' 章' : '加载中...'; } else { btnDownloadAll.style.display = ''; tocProgress.style.display = 'none'; } } function setTocBtnStopping() { btnLoadToc.textContent = '停止'; btnLoadToc.style.opacity = '1'; btnLoadToc.style.pointerEvents = 'auto'; btnLoadToc.style.cursor = 'pointer'; } function resetTocBtn() { btnLoadToc.textContent = '加载目录页'; btnLoadToc.style.opacity = ''; btnLoadToc.style.pointerEvents = ''; btnLoadToc.style.cursor = ''; } function startTocFetch() { tocLoading = true; updateTocUI(); setTocBtnStopping(); send({ type: 'fetchToc', url: tocUrl }); } function catalogItems() { if (tocData && tocData.length) return tocData; return C.map(function (c, i) { return { title: c.title || ('章节 ' + (i + 1)), url: c.url }; }); } function currentUrl() { return C.length && C[d] ? C[d].url : ''; } function renderCatalog() { _.innerHTML = ''; var items = catalogItems(); if (!items.length) { _.innerHTML = '
' + STR.noChapters + '
'; return; } var seen = {}, unique = []; items.forEach(function (it) { if (!it.url) return; if (seen[it.url]) return; seen[it.url] = true; unique.push(it); }); items = unique; var cur = currentUrl(); var hasVolumeAndChapter = items.some(function (it) { var t = it.title || ''; return /卷.*章/.test(t); }); function createItem(it, isActive) { var el = document.createElement('div'); el.className = 'catalog-item' + (isActive ? ' active' : ''); if (it.url) el.dataset.tocUrl = it.url; if (selectMode) { el.style.display = 'flex'; el.style.alignItems = 'center'; var cb = document.createElement('input'); cb.type = 'checkbox'; cb.className = 'dl-check'; cb.dataset.dlUrl = it.url || ''; cb.checked = !!selectedUrls[it.url]; cb.style.cssText = 'margin:0 8px 0 0;flex:none;width:16px;height:16px;cursor:pointer;'; cb.addEventListener('click', function (ev) { ev.stopPropagation(); }); cb.addEventListener('change', function () { if (cb.checked) selectedUrls[it.url] = true; else delete selectedUrls[it.url]; updateSelCount(); }); el.appendChild(cb); el.appendChild(document.createTextNode(it.title)); el.addEventListener('click', function () { cb.checked = !cb.checked; if (cb.checked) selectedUrls[it.url] = true; else delete selectedUrls[it.url]; updateSelCount(); }); return el; } el.textContent = it.title; el.addEventListener('click', function () { closeCatalog(); var idx = findLoadedByUrl(it.url); if (idx >= 0) { var c = C[idx]; if (c && c.element) { c.element.scrollIntoView({ behavior: 'smooth', block: 'start' }); d = idx; v.textContent = c.title || STR.readerMode; sendCurrent(c.url, c.title); markActive(); } } else if (it.url) { showLoading(STR.loadingJump); send({ type: 'fetchChapter', url: it.url, mode: 'jump' }); } }); return el; } var frag = document.createDocumentFragment(); if (hasVolumeAndChapter) { items.forEach(function (it) { frag.appendChild(createItem(it, it.url === cur)); }); _.appendChild(frag); markActive(); return; } var normal = [], special = []; items.forEach(function (it) { var num = parseChapterNumber(it.title); if (num !== null && num > 0) { normal.push({ item: it, num: num }); } else { special.push(it); } }); normal.sort(function (a, b) { return a.num - b.num; }); normal.forEach(function (o2) { frag.appendChild(createItem(o2.item, o2.item.url === cur)); }); if (special.length) { var toggleBtn = document.createElement('div'); toggleBtn.className = 'catalog-toggle'; toggleBtn.textContent = '▼ 其他章节 (' + special.length + ')'; toggleBtn.style.cssText = 'padding:8px 20px;font-size:13px;opacity:.6;cursor:pointer;border-top:1px solid rgba(128,128,128,.2);margin-top:8px;user-select:none;'; toggleBtn.addEventListener('click', function (ev) { ev.stopPropagation(); var container = this.nextElementSibling; if (container.style.display === 'none') { container.style.display = 'block'; this.textContent = '▲ 其他章节 (' + special.length + ')'; } else { container.style.display = 'none'; this.textContent = '▼ 其他章节 (' + special.length + ')'; } }); frag.appendChild(toggleBtn); var specialContainer = document.createElement('div'); specialContainer.className = 'special-chapters'; specialContainer.style.display = 'none'; special.forEach(function (it) { specialContainer.appendChild(createItem(it, it.url === cur)); }); frag.appendChild(specialContainer); } _.appendChild(frag); markActive(); } function markActive() { var cur = currentUrl(); if (markActivePrev && markActivePrev.dataset && markActivePrev.dataset.tocUrl === cur) return; if (markActivePrev) { try { markActivePrev.classList.remove('active'); } catch (e) {} markActivePrev = null; } var el = _.querySelector('.catalog-item.active'); if (el) { try { el.classList.remove('active'); } catch (e) {} } if (!cur) return; var hit = _.querySelector('.catalog-item[data-toc-url="' + cur.replace(/"/g, '%22') + '"]'); if (hit) { hit.classList.add('active'); markActivePrev = hit; } } function openCatalog() { renderCatalog(); n.classList.add('visible'); r.classList.add('visible'); hideBars(); var act = _.querySelector('.catalog-item.active'); if (act) { try { act.scrollIntoView({ block: 'center' }); } catch (e2) {} } if (tocData.length === 0 && tocUrl && !tocLoading) startTocFetch(); } function closeCatalog() { n.classList.remove('visible'); r.classList.remove('visible'); } function checkAutoLoad() { if (C.length === 0 || stitching) return; if (E.scrollHeight - (E.scrollTop + E.clientHeight) < 800) { var last = C[C.length - 1]; if (last && last.nextUrl) M(last.url, last.nextUrl); } } function onScroll() { var st = E.scrollTop; if (Math.abs(st - lastScrollTop) > 2 && o) { o = false; m.classList.remove('visible'); h.classList.remove('visible'); } lastScrollTop = st; checkAutoLoad(); for (var i = C.length - 1; i >= 0; i--) { if (C[i].element.getBoundingClientRect().top <= 80) { if (d !== i) { d = i; v.textContent = C[i].title || STR.readerMode; sendCurrent(C[i].url, C[i].title); markActive(); } break; } } } E.addEventListener('scroll', function () { if (scrollTicking) return; scrollTicking = true; requestAnimationFrame(function () { scrollTicking = false; onScroll(); }); }); function toggleBars() { if (G) { closeSettings(); return; } if (n.classList.contains('visible')) { closeCatalog(); return; } if (extraVisible) { closeExtraPanel(); return; } o = !o; m.classList.toggle('visible', o); h.classList.toggle('visible', o); } E.addEventListener('click', function (ev) { var aEl = (ev.target && ev.target.closest) ? ev.target.closest('a') : null; if (aEl) { ev.preventDefault(); return; } if (o || G) { toggleBars(); return; } var rect = E.getBoundingClientRect(); var yy = ev.clientY - rect.top, hh = rect.height, step = hh - 40; if (yy < hh * 0.3) E.scrollBy({ top: -step, behavior: 'smooth' }); else if (yy > hh * 0.7) E.scrollBy({ top: step, behavior: 'smooth' }); else toggleBars(); }); e.addEventListener('click', function () { toggleBars(); }); document.getElementById('btnBack').addEventListener('click', function (ev) { ev.stopPropagation(); send({ type: 'exit' }); }); document.getElementById('btnCatalog').addEventListener('click', function (ev) { ev.stopPropagation(); openCatalog(); }); r.addEventListener('click', function () { closeCatalog(); }); btnLoadToc.addEventListener('click', function (ev) { ev.stopPropagation(); if (!tocUrl) return; if (tocLoading) { send({ type: 'stopFetchToc' }); return; } startTocFetch(); }); function catalogMap() { var map = {}; catalogItems().forEach(function (it) { if (it.url) map[it.url] = it.title || it.url; }); return map; } function allChecks() { return Array.prototype.slice.call(_.querySelectorAll('.dl-check')); } function countSelected() { var cnt = 0; allChecks().forEach(function (c) { if (c.checked) cnt++; }); return cnt; } function updateSelCount() { var nSel = countSelected(); var btn = document.getElementById('dlSelStart'); if (btn) btn.textContent = '开始下载 (' + nSel + ')'; var all = document.getElementById('dlSelAll'); if (all) { var checks = allChecks(); all.checked = checks.length > 0 && nSel === checks.length; } } function toggleAllChecks(on) { allChecks().forEach(function (cb) { cb.checked = on; if (on) selectedUrls[cb.dataset.dlUrl] = true; else delete selectedUrls[cb.dataset.dlUrl]; }); updateSelCount(); } function selectRange(a, b) { var checks = allChecks(); toggleAllChecks(false); var i0 = Math.max(1, a) - 1; var i1 = Math.min(checks.length, b); for (var i = i0; i < i1; i++) { checks[i].checked = true; selectedUrls[checks[i].dataset.dlUrl] = true; } updateSelCount(); } function resetDlBtn() { dlRunning = false; btnDownloadAll.textContent = '下载全部'; btnDownloadAll.disabled = false; } function removeSelectToolbar() { if (selectToolbar && selectToolbar.parentNode) selectToolbar.parentNode.removeChild(selectToolbar); selectToolbar = null; } function mkDlBtn(text, primary) { var btn = document.createElement('button'); btn.textContent = text; btn.style.cssText = primary ? 'padding:5px 12px;border:none;border-radius:6px;background:#d9822b;color:#fff;cursor:pointer;font-size:12px;font-weight:500;' : 'padding:5px 10px;border:1px solid rgba(128,128,128,.4);border-radius:6px;background:transparent;cursor:pointer;font-size:12px;color:inherit;'; return btn; } function buildSelectToolbar() { removeSelectToolbar(); var tb = document.createElement('div'); tb.id = 'dlSelectToolbar'; tb.style.cssText = 'padding:8px 12px;border-bottom:1px solid rgba(128,128,128,.25);display:flex;flex-wrap:wrap;gap:6px;align-items:center;font-size:13px;flex:none;'; var lab = document.createElement('label'); lab.style.cssText = 'display:flex;align-items:center;gap:4px;cursor:pointer;'; var cbAll = document.createElement('input'); cbAll.type = 'checkbox'; cbAll.id = 'dlSelAll'; cbAll.style.cssText = 'width:15px;height:15px;cursor:pointer;'; cbAll.addEventListener('change', function () { toggleAllChecks(cbAll.checked); }); lab.appendChild(cbAll); lab.appendChild(document.createTextNode('全选')); tb.appendChild(lab); var sp1 = document.createElement('span'); sp1.textContent = '从'; tb.appendChild(sp1); var inpFrom = document.createElement('input'); inpFrom.type = 'number'; inpFrom.min = '1'; inpFrom.value = '1'; inpFrom.style.cssText = 'width:54px;padding:3px 4px;border:1px solid rgba(128,128,128,.4);border-radius:4px;background:transparent;color:inherit;font-size:12px;'; tb.appendChild(inpFrom); var sp2 = document.createElement('span'); sp2.textContent = '到'; tb.appendChild(sp2); var inpTo = document.createElement('input'); inpTo.type = 'number'; inpTo.min = '1'; inpTo.value = '1'; inpTo.style.cssText = inpFrom.style.cssText; tb.appendChild(inpTo); var btnRange = mkDlBtn('选范围', false); btnRange.addEventListener('click', function (ev) { ev.stopPropagation(); var a = parseInt(inpFrom.value, 10) || 1; var b = parseInt(inpTo.value, 10) || 1; if (a > b) { var t = a; a = b; b = t; } selectRange(a, b); }); tb.appendChild(btnRange); var btnStart = mkDlBtn('开始下载 (0)', true); btnStart.id = 'dlSelStart'; btnStart.addEventListener('click', function (ev) { ev.stopPropagation(); startSelectedDownload(); }); tb.appendChild(btnStart); var btnCancel = mkDlBtn('取消', false); btnCancel.addEventListener('click', function (ev) { ev.stopPropagation(); exitSelectMode(); }); tb.appendChild(btnCancel); if (_.parentNode) _.parentNode.insertBefore(tb, _); selectToolbar = tb; } function enterSelectMode() { selectMode = true; selectedUrls = {}; btnDownloadAll.textContent = '选择章节...'; btnDownloadAll.disabled = true; buildSelectToolbar(); renderCatalog(); } function exitSelectMode() { selectMode = false; selectedUrls = {}; removeSelectToolbar(); btnDownloadAll.textContent = STR.download; btnDownloadAll.disabled = false; renderCatalog(); } function startSelectedDownload() { var checks = allChecks(); var map = catalogMap(); var list = []; checks.forEach(function (cb) { if (cb.checked) list.push({ url: cb.dataset.dlUrl, title: map[cb.dataset.dlUrl] || '' }); }); if (!list.length) { toast('请先勾选要下载的章节'); return; } selectMode = false; removeSelectToolbar(); dlRunning = true; btnDownloadAll.textContent = '下载中 0/' + list.length; btnDownloadAll.disabled = false; renderCatalog(); send({ type: 'downloadAll', chapters: list }); } btnDownloadAll.addEventListener('click', function (ev) { ev.stopPropagation(); if (dlRunning) { if (btnDownloadAll.textContent.indexOf('停止中') !== -1) return; btnDownloadAll.textContent = '停止中...'; btnDownloadAll.disabled = true; toast('正在停止下载...'); send({ type: 'downloadPause' }); return; } if (selectMode) return; var list = (tocData && tocData.length >= 20) ? tocData : C.slice(); if (!list || list.length === 0) { toast('暂无章节可下载'); return; } enterSelectMode(); }); function syncNightBtn() { if (s.theme === 'dark') { btnNight.querySelector('span').textContent = STR.day; btnNight.classList.add('active'); } else { btnNight.querySelector('span').textContent = STR.night; btnNight.classList.remove('active'); } } btnNight.addEventListener('click', function (ev) { ev.stopPropagation(); if (s.theme === 'dark') { s.theme = s.prevTheme || 'warm'; } else { s.prevTheme = s.theme; s.theme = 'dark'; } document.body.className = 'theme-' + s.theme; syncNightBtn(); syncThemeDots(); saveSettings(); }); function openSettings() { G = true; Y.classList.add('visible'); j.classList.add('visible'); hideBars(); } function closeSettings() { G = false; Y.classList.remove('visible'); j.classList.remove('visible'); } document.getElementById('btnSettings').addEventListener('click', function (ev) { ev.stopPropagation(); openSettings(); }); j.addEventListener('click', function () { closeSettings(); }); document.getElementById('btnManagePurify').addEventListener('click', function (ev) { ev.stopPropagation(); closeSettings(); openExtraPanel('purifyPanel'); }); document.getElementById('purifyClose').addEventListener('click', function (ev) { ev.stopPropagation(); closeExtraPanel(); }); document.getElementById('purifyAddBtn').addEventListener('click', function (ev) { ev.stopPropagation(); var inp = document.getElementById('purifyInput'); var v = (inp.value || '').trim(); if (!v) return; send({ type: 'addPurifyWords', words: v }); inp.value = ''; }); document.getElementById('purifyInput').addEventListener('keydown', function (ev) { if (ev.key === 'Enter') { ev.stopPropagation(); var v = (this.value || '').trim(); if (!v) return; send({ type: 'addPurifyWords', words: v }); this.value = ''; } }); /* 净化说明:默认收起,点标题展开/收起,标题栏带箭头标识 */ (function () { var help = document.getElementById('purifyHelp'); if (!help) return; var raw = help.innerHTML; // 去掉原标题
,只保留正文说明 raw = raw.replace(/^\s*[\s\S]*?<\/b>\s*/i, ''); var arrow = document.createElement('span'); arrow.id = 'purifyHelpArrow'; arrow.textContent = '▾'; arrow.style.cssText = 'flex:none;font-size:12px;line-height:1;display:inline-block;color:inherit;opacity:.7;transition:transform .25s;transform:rotate(-90deg);'; var head = document.createElement('div'); head.id = 'purifyHelpHead'; head.style.cssText = 'display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:pointer;user-select:none;-webkit-user-select:none;'; var title = document.createElement('b'); title.textContent = '净化说明(阅读和下载TXT都会生效)'; head.appendChild(title); head.appendChild(arrow); var body = document.createElement('div'); body.id = 'purifyHelpBody'; body.style.cssText = 'margin-top:8px;display:none;'; body.innerHTML = raw; help.innerHTML = ''; help.appendChild(head); help.appendChild(body); head.addEventListener('click', function (ev) { ev.stopPropagation(); var collapsed = body.style.display === 'none'; body.style.display = collapsed ? 'block' : 'none'; arrow.style.transform = collapsed ? 'rotate(0deg)' : 'rotate(-90deg)'; }); })(); function applyFont() { b.textContent = s.fontSize; var cs = f.querySelectorAll('.chapter-content'); for (var i = 0; i < cs.length; i++) cs[i].style.fontSize = s.fontSize + 'px'; var ts = f.querySelectorAll('.chapter-title'); for (var k = 0; k < ts.length; k++) ts[k].style.fontSize = (s.fontSize + 4) + 'px'; } function syncThemeDots() { var dots = document.querySelectorAll('.theme-dot'); for (var i = 0; i < dots.length; i++) dots[i].classList.toggle('active', dots[i].dataset.theme === s.theme); } function applySettings() { document.body.className = 'theme-' + s.theme; applyFont(); var btns = document.querySelectorAll('#spacingBtns button'); for (var i = 0; i < btns.length; i++) btns[i].classList.toggle('active', parseFloat(btns[i].dataset.val) === s.lineHeight); syncThemeDots(); syncNightBtn(); var cs = f.querySelectorAll('.chapter-content'); for (var k = 0; k < cs.length; k++) cs[k].style.lineHeight = s.lineHeight; } function saveSettings() { send({ type: 'saveSettings', settings: s }); } document.getElementById('fontDec').addEventListener('click', function (ev) { ev.stopPropagation(); s.fontSize = Math.max(12, s.fontSize - 2); applyFont(); saveSettings(); }); document.getElementById('fontInc').addEventListener('click', function (ev) { ev.stopPropagation(); s.fontSize = Math.min(32, s.fontSize + 2); applyFont(); saveSettings(); }); document.getElementById('spacingBtns').addEventListener('click', function (ev) { var tgt = ev.target; if (!tgt.dataset || !tgt.dataset.val) return; ev.stopPropagation(); var btns = this.querySelectorAll('button'); for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active'); tgt.classList.add('active'); s.lineHeight = parseFloat(tgt.dataset.val); var cs = f.querySelectorAll('.chapter-content'); for (var k = 0; k < cs.length; k++) cs[k].style.lineHeight = s.lineHeight; saveSettings(); }); document.getElementById('themeDots').addEventListener('click', function (ev) { var tgt = ev.target; if (!tgt.dataset || !tgt.dataset.theme) return; ev.stopPropagation(); s.theme = tgt.dataset.theme; document.body.className = 'theme-' + s.theme; syncThemeDots(); if (s.theme !== 'dark') s.prevTheme = s.theme; syncNightBtn(); saveSettings(); }); function closeExtraPanel() { extraVisible = false; whitelistPanel.classList.remove('visible'); historyPanel.classList.remove('visible'); var pp = document.getElementById('purifyPanel'); if (pp) pp.classList.remove('visible'); extraMask.style.display = 'none'; } function openExtraPanel(panelId) { closeExtraPanel(); extraVisible = true; var panel = document.getElementById(panelId); if (!panel) return; panel.classList.add('visible'); extraMask.style.display = 'block'; if (panelId === 'whitelistPanel') { send({ type: 'getWhitelist' }); } else if (panelId === 'historyPanel') { send({ type: 'getHistory' }); } else if (panelId === 'purifyPanel') { send({ type: 'getPurify' }); } setTimeout(function () { var list = panel.querySelector('.panel-list'); if (list) setupFade(list); }, 50); } function bindExtraToggle(btnId, panelId) { document.getElementById(btnId).addEventListener('click', function (ev) { ev.stopPropagation(); var panel = document.getElementById(panelId); if (extraVisible && panel.classList.contains('visible')) { closeExtraPanel(); return; } openExtraPanel(panelId); }); } document.getElementById('whitelistClose').addEventListener('click', function (ev) { ev.stopPropagation(); closeExtraPanel(); }); document.getElementById('historyClose').addEventListener('click', function (ev) { ev.stopPropagation(); closeExtraPanel(); }); extraMask.addEventListener('click', function () { closeExtraPanel(); }); bindExtraToggle('btnWhitelist', 'whitelistPanel'); bindExtraToggle('btnHistory', 'historyPanel'); function setupFade(list) { var wrap = list.parentNode; if (!wrap) return; var topMask = wrap.querySelector('.fade-mask.top'); var bottomMask = wrap.querySelector('.fade-mask.bottom'); if (!topMask || !bottomMask) return; function checkFade() { var scrollTop = list.scrollTop; var maxScroll = list.scrollHeight - list.clientHeight; topMask.classList.toggle('hidden', scrollTop <= 2); bottomMask.classList.toggle('hidden', maxScroll - scrollTop <= 2); } list.addEventListener('scroll', checkFade); setTimeout(checkFade, 100); var observer = new MutationObserver(function () { checkFade(); }); observer.observe(list, { childList: true, subtree: false }); list._fadeObserver = observer; list._fadeCleanup = function () { observer.disconnect(); list.removeEventListener('scroll', checkFade); }; } function buildPanelItem(titleText, highlighted, onMainClick, onDelClick) { var item = document.createElement('div'); item.className = 'panel-item'; if (highlighted) { item.style.backgroundColor = getHighlightColor(); item.classList.add('panel-item-hi'); } var main = document.createElement('div'); main.className = 'item-main'; var titleSpan = document.createElement('span'); titleSpan.className = 'item-title'; titleSpan.textContent = titleText; main.appendChild(titleSpan); var delBtn = document.createElement('button'); delBtn.className = 'item-del'; delBtn.textContent = '✕'; delBtn.setAttribute('aria-label', '删除'); delBtn.addEventListener('click', function (ev) { ev.stopPropagation(); onDelClick(); }); main.addEventListener('click', onMainClick); item.appendChild(main); item.appendChild(delBtn); return item; } function renderPanelList(containerId, emptyHtml, items) { var container = document.getElementById(containerId); if (container._fadeCleanup) { container._fadeCleanup(); container._fadeCleanup = null; } container.innerHTML = ''; if (!items || items.length === 0) { container.innerHTML = '
' + emptyHtml + '
'; return; } items.forEach(function (item) { container.appendChild(item); }); setupFade(container); } function renderWhitelist(list) { var items = (list || []).map(function (entry) { var domain = entry.domain || ''; return buildPanelItem(entry.title || domain, domain === currentHost, function () { send({ type: 'navigateTo', url: 'https://' + domain }); }, function () { send({ type: 'removeWhitelist', domain: domain }); }); }); renderPanelList('whitelistList', '暂无白名单', items); // 当前网站高亮项可能被挤到列表底部不可见:打开面板后自动滚动到可视区域(居中) setTimeout(function () { var hi = document.getElementById('whitelistList').querySelector('.panel-item-hi'); if (hi) { try { hi.scrollIntoView({ block: 'center' }); } catch (e) { hi.scrollIntoView(); } } }, 100); } function renderHistory(list) { var sorted = (list || []).slice().sort(function (a, b) { return b.timestamp - a.timestamp; }); var items = sorted.map(function (entry) { var entryKey = entry.bookKey || getBookRootKey(entry.url); return buildPanelItem(entry.title || '未命名', entryKey === currentBookKey, function () { send({ type: 'navigateTo', url: entry.url }); }, function () { send({ type: 'removeHistory', url: entry.url }); }); }); renderPanelList('historyList', '暂无历史记录', items); } function renderPurify(list) { var items = (list || []).map(function (w) { return buildPanelItem(w, false, function () {}, function () { send({ type: 'removePurifyWord', word: w }); }); }); renderPanelList('purifyList', '暂无净化词', items); var pspan = document.getElementById('purifyPanel').querySelector('.panel-header span'); if (pspan) pspan.textContent = '净化词(' + (list || []).length + ')· 点✕删除'; } send({ type: 'ready' }); } var READER_JS = "(function () {\n'use strict';\n" + SHARED_SRC + "\n" + iframeMain.toString() + "\niframeMain();\n})();"; var READER_HTML = '' + '' + '阅读模式' + '' + READER_BODY + '