// ==UserScript== // @name X阅读模式 // @namespace https://scriptcat.org/scripts/code/7614/X阅读模式.user.js // @version 1.3.9 // @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); try { out.push('目录缓存数量: ' + Object.keys(getTocCacheAll()).length); } catch (eTc) {} 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 KEY_TOC_CACHE = 'novel_reader_toc_cache_v1'; var TOC_CACHE_TTL = 24 * 60 * 60 * 1000; // 目录缓存有效期:24小时 var TOC_CACHE_MAX = 10; // 目录缓存最多保留10本书(LRU淘汰) 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; var _tocBusy = 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) {} } /* ==================== 目录缓存(最近10本书,24小时过期) ==================== 结构: { [bookKey]: { toc:[{title,url}], tocUrl:'', title:'', savedAt:时间戳 } } 进入正文页时若命中未过期缓存,直接用缓存目录,不再重新抓取目录页。 每次读写都会清理:超过24小时的条目删除;超过10本时按 savedAt 最旧的 LRU 淘汰。 */ function getTocCacheAll() { try { var v = GM_getValue(KEY_TOC_CACHE, '{}'); var o = v ? JSON.parse(v) : null; return (o && typeof o === 'object' && !Array.isArray(o)) ? o : {}; } catch (e) { return {}; } } function saveTocCacheAll(o) { try { GM_setValue(KEY_TOC_CACHE, JSON.stringify(o || {})); } catch (e) {} } function pruneTocCache(cache) { if (!cache || typeof cache !== 'object') return {}; var now = Date.now(); var keys = Object.keys(cache); for (var i = 0; i < keys.length; i++) { var k = keys[i], it = cache[k]; if (!it || !Array.isArray(it.toc) || !it.savedAt || now - it.savedAt > TOC_CACHE_TTL) { delete cache[k]; } } var remain = Object.keys(cache); if (remain.length > TOC_CACHE_MAX) { remain.sort(function (a, b) { return cache[a].savedAt - cache[b].savedAt; }); var drop = remain.length - TOC_CACHE_MAX; for (var j = 0; j < drop; j++) delete cache[remain[j]]; } return cache; } function getTocCacheForBook(bookKey) { if (!bookKey) return null; var cache = getTocCacheAll(); var it = cache[bookKey]; if (!it || !Array.isArray(it.toc) || !it.savedAt) return null; if (Date.now() - it.savedAt > TOC_CACHE_TTL) { delete cache[bookKey]; saveTocCacheAll(cache); return null; } return it; } function saveTocCacheForBook(bookKey, toc, tocUrl, title) { if (!bookKey || !toc || !toc.length) return; var cache = getTocCacheAll(); cache[bookKey] = { toc: toc, tocUrl: tocUrl || '', title: title || '', savedAt: Date.now() }; pruneTocCache(cache); saveTocCacheAll(cache); } function removeTocCacheForBook(bookKey) { if (!bookKey) return; var cache = getTocCacheAll(); if (cache[bookKey]) { delete cache[bookKey]; saveTocCacheAll(cache); } } 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(); } } // ==================== 分页噪声行判定(下载与阅读正文共用) ==================== // 页码标记:第X章(第Y页)/(第Y页)/第 X/Y 页 / 裸 X/Y function isPageMarkerLine(t) { if (!t) return false; var s = String(t).replace(/\s+/g, ''); if (!s) return false; if (/^第[0-9零一二三四五六七八九十百千万亿两]+[章节回卷话篇集]/.test(s) && /[((]第.*?页[))]/.test(s)) return true; if (/^[((]第.*?页[))]$/.test(s)) return true; if (/^第\s*[((]?\s*\d+\s*\/\s*\d+\s*[))]?\s*页$/.test(s)) return true; if (/^第\s*\d+\s*页$/.test(s)) return true; return false; } // “本章未完,下一页继续阅读”类分页提示行判定(比旧版做更多适配) function isNextPagePromptLine(t) { if (!t) return false; var s = String(t).replace(/\s+/g, ''); if (!s) return false; // ① 本章/正文/本书/内容 + 未完(未完结/尚未结束/待续…) + 下一页/下页/翻页/继续阅读/点击/翻 if (/(本章|正文|本书|内容)/.test(s) && /(未完|未完结|尚未结束|没有结束|未结束|待续)/.test(s) && /(下一页|下页|翻页|翻到|继续阅读|点击|请翻|下一頁|下頁|翻頁)/.test(s)) return true; // ② “未完待续”独立短句(含括号包裹) if (/^[((【\[]?(本章)?未完待续[))】\]]?[,。,!!??…~~\s]*$/.test(s)) return true; // ③ “下一页/下页/翻页 + 继续阅读”独立短句(含“点击/请翻到”引导、“继续阅读本章”等后缀) if (/^(点击|请|请点|轻点|点)?(翻到|前往|点击)?(下一页|下页|翻页|下一頁|下頁|翻頁)(继续阅读|继续|阅读|查看)?(本章内容|本章|本页|正文)?[。!!??…~\s]*$/.test(s)) return true; if (/^(请|点|点击)?(翻到|前往)?(下一页|下页|翻页|下一頁|下頁|翻頁)[^。!?\n]{0,10}(继续|继续阅读|阅读)[。!!??…~\s]*$/.test(s)) return true; return false; } function isPageNoiseLine(t) { return isPageMarkerLine(t) || isNextPagePromptLine(t); } 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(); if (isPageNoiseLine(line)) continue; 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; } } // 目录页章节名解析(阅读正文使用): // 正文里显示的章节名改为“从目录页抓取的章节名”,替代正文页自身的 title。 // normReaderUrl:URL 归一化(去 hash/query/末尾斜杠;并保留“再去 .html/.htm”的第二级),兼容目录与正文 URL 的细微差异。 // 返回 [原级, 去.html级] 两个字符串;tocTitleForUrl 中任一级相等即视为同一章节。 function normReaderUrl(u) { if (!u) return null; var s = String(u); try { s = new URL(s).href; } catch (e) {} s = s.split('#')[0].split('?')[0].replace(/\/+$/, ''); return [s, s.replace(/\.html?$/i, '')]; } // 在目录数组([{title,url},...])中找与当前章节对应的目录名;未命中返回 ''(由调用方回退正文 title)。 // 两级匹配: // ① URL 整体等价匹配(原级/去.html级),绝不子串匹配,避免 “123” 误配到 “12345” 之类的错章; // ② 章节号兜底:目录与正文的 URL 可能不一致,但“第N章”语义一致——正文 title 的章号命中目录条目的章号, // 就用该目录条目的完整章节名(如正文 title “第101章 避无可避 (第1/2页)” → 目录 “第101章 避无可避”)。 function tocTitleForUrl(toc, url, bodyTitle) { if (!toc || !toc.length) return ''; if (url) { var target = normReaderUrl(url); if (target) { var a = target[0], b = target[1]; for (var i = 0; i < toc.length; i++) { var it = toc[i]; if (!it || !it.url) continue; var t = normReaderUrl(it.url); if (!t) continue; if (t[0] === a || t[0] === b || t[1] === a || t[1] === b) return it.title || ''; } } } if (bodyTitle) { var bn = parseChapterNumber(bodyTitle); if (bn !== null && bn > 0) { for (var j = 0; j < toc.length; j++) { var tj = toc[j]; if (!tj || !tj.title) continue; var tn = parseChapterNumber(tj.title); if (tn !== null && tn === bn) return tj.title || ''; } } } return ''; } // 下载文本净化:按行(段落)分别净化,与阅读器逐文本节点净化保持一致 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'); } // ==================== 阅读正文清洗(与下载清洗同规则,但保留 HTML 结构) ==================== // 收集根内所有“有文字、无元素子节点”的叶子元素(按文档顺序;跳过 img/br) function collectLeafTextEls(root) { var list = []; (function walk(node) { var kids = node.childNodes; for (var i = 0; i < kids.length; i++) { var c = kids[i]; if (c.nodeType !== 1) continue; var el = c; if (el.tagName === 'IMG' || el.tagName === 'BR') continue; var hasElChild = false; for (var j = 0; j < el.childNodes.length; j++) { var cc = el.childNodes[j]; if (cc.nodeType === 1 && cc.tagName !== 'BR') { hasElChild = true; break; } } if (!hasElChild && (el.textContent || '').replace(/\s+/g, '').trim()) list.push(el); walk(el); } })(root); return list; } // 只对根内“首部/尾部”叶子元素删除分页噪声,正文中间不碰(避免误杀正文)。 // 分页站的页码、未完下一页提示都出现在每页首尾,正文中间极少出现这类整行。 function removePageNoiseLeafs(root, maxHead, maxTail) { var leaves = collectLeafTextEls(root); if (!leaves.length) return; var h = Math.min(leaves.length, maxHead || 8); var t = Math.min(leaves.length, maxTail || 8); for (var j = 0; j < leaves.length; j++) { var inHead = j < h; var inTail = j >= leaves.length - t; if (!inHead && !inTail) continue; var el = leaves[j]; if (el.querySelector && el.querySelector('img')) continue; var text = (el.textContent || '').replace(/\s+/g, '').trim(); if (!text) continue; if (isPageNoiseLine(text)) el.parentNode.removeChild(el); } } // 删除正文开头“章节标题行”叶子元素(与下载 stripLeadingChapterHeading 同规则;只匹配开头) // 首页分页专用:.chapter-title 已单独显示标题,正文里源站写的“第X章…”标题行必须删掉,避免重复。 // 扫描开头最多 6 个叶子,命中“章节标题行”即删;正文句(带标点/超长/不以章名开头)不会命中。 function stripLeadingHeadingDom(root) { var re = /^(第\s*[0-9零一二三四五六七八九十百千两〇○]+\s*[章节節回卷話篇集]|chapter\s*\d+|序章|楔子|尾声|后记|番外)(\s|[::]|$)/i; var leaves = collectLeafTextEls(root); var maxScan = Math.min(leaves.length, 6); for (var k = 0; k < maxScan; k++) { var el = leaves[k]; if (!el || !el.parentNode) continue; var text = (el.textContent || '').trim(); if (!text) continue; if (text.length <= 30 && re.test(text) && !/[。!?,;、]/.test(text)) { el.parentNode.removeChild(el); } } } // 阅读正文 HTML 清洗:去分页噪声 → 去开头章节标题 → 净化词 function cleanReaderHtml(html, words, opts) { if (!html) return html; try { var doc = new DOMParser().parseFromString('
' + html + '
', 'text/html'); var root = doc.getElementById('__reader_clean_root'); if (!root) return html; removePageNoiseLeafs(root); if (!opts || opts.stripLeadingHeading !== false) stripLeadingHeadingDom(root); purifyDomElement(root, words); return root.innerHTML; } catch (e) { return html; } } 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, normReaderUrl, tocTitleForUrl] .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(); refreshRenderedTitles(); // 目录提前加载:一切换到阅读模式界面即开始抓取目录,无需等点击右上角三条横杠图标。 // 受 tocLoading 保护,不会与打开目录时的延迟触发重复抓取。 if (tocUrl && (!tocData || tocData.length === 0) && !tocLoading) { startTocFetch(); } 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(); refreshRenderedTitles(); if (n.classList.contains('visible')) scrollActiveIntoCenter(); 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) {} } } // 去掉章节标题里的分页页码后缀((2/3)、(第2页)、第2页 等) function stripPageSuffix(t) { return String(t || '') .replace(/[((【\[]\s*第?\s*[0-9一二三四五六七八九十百千]+\s*\/\s*[0-9一二三四五六七八九十百千]+\s*页?\s*[))】\]]/g, '') .replace(/[((【\[]\s*第\s*[0-9一二三四五六七八九十百千万亿两]+\s*页\s*[))】\]]/g, '') .replace(/第\s*[0-9一二三四五六七八九十百千万亿两]+\s*页$/g, '') .replace(/\s+/g, '') .trim(); } // 判断两个标题是否属于同一章节(分页标题:第1章 vs 第1章(2/3)) function sameChapterTitle(a, b) { if (!a || !b) return false; var ca = String(a).replace(/\s+/g, '').trim(); var cb = String(b).replace(/\s+/g, '').trim(); if (!ca || !cb) return false; if (ca === cb) return true; var n1 = parseChapterNumber(ca); var n2 = parseChapterNumber(cb); if (n1 !== null && n2 !== null && n1 === n2) return true; var ra = stripPageSuffix(ca); var rb = stripPageSuffix(cb); return ra.length > 0 && ra === rb; } function J(t, first) { if (!t || !t.url) return; // 正文章节名优先取“目录页抓取的章节名”(URL 匹配或章节号兜底),未命中才回退正文页 title var dispTitle = tocTitleForUrl(tocData, t.url, t.title) || t.title || ''; // 同一章节的分页:直接并入上一章块,不再重复渲染章节标题/分隔线(仅章节首页分页保留标题) var prev = C.length ? C[C.length - 1] : null; var isPage = !first && prev && sameChapterTitle(prev.title, dispTitle); if (isPage) { var cont = prev.element.querySelector('.chapter-content'); if (cont) { var tmp = document.createElement('div'); tmp.innerHTML = purifyHtml(t.data || '', purifyWords); sanitize(tmp); filterImages(tmp); while (tmp.firstChild) cont.appendChild(tmp.firstChild); var imgs = tmp.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]); } } prev.url = t.url || prev.url; prev.nextUrl = t.next_url || prev.nextUrl; renderCatalog(); 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 = dispTitle || STR.nextChapter; divider.appendChild(dt); block.appendChild(divider); } var titleEl = document.createElement('div'); titleEl.className = 'chapter-title'; titleEl.textContent = dispTitle; 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: dispTitle, 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(dispTitle); E.scrollTop = 0; sendCurrent(t.url, dispTitle); } renderCatalog(); } // 目录加载完成后,把已渲染章节块的标题/分隔线/顶栏统一刷新为目录页章节名 function refreshRenderedTitles() { var i, item, tocT, blk, tEl, dEl; for (i = 0; i < C.length; i++) { item = C[i]; tocT = tocTitleForUrl(tocData, item.url, item.title); if (!tocT || tocT === item.title) continue; item.title = tocT; blk = item.element; if (!blk) continue; tEl = blk.querySelector('.chapter-title'); if (tEl) tEl.textContent = tocT; dEl = blk.querySelector('.chapter-divider-title'); if (dEl) dEl.textContent = tocT; } if (C.length && C[d]) { v.textContent = C[d].title || STR.readerMode; sendCurrent(C[d].url, C[d].title); } } 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(); } // 目录条目与当前章节是否对应:URL 归一化等价,或章节号一致(目录/正文 URL 可能不一致) function isSameTocEntry(tocUrl, tocTitle, curUrl, curTitle) { if (curUrl && tocUrl) { var t = normReaderUrl(tocUrl), c = normReaderUrl(curUrl); if (t && c && (t[0] === c[0] || t[0] === c[1] || t[1] === c[0] || t[1] === c[1])) return true; } if (curTitle && tocTitle) { var bn = parseChapterNumber(curTitle); if (bn !== null && bn > 0) { var tn = parseChapterNumber(tocTitle); if (tn !== null && tn === bn) return true; } } return false; } function markActive() { var cur = currentUrl(); var curTitle = (C.length && C[d]) ? (C[d].title || '') : ''; var hit = null; if (cur || curTitle) { var items = _.querySelectorAll('.catalog-item'); for (var i = 0; i < items.length; i++) { if (isSameTocEntry(items[i].dataset.tocUrl || '', items[i].textContent || '', cur, curTitle)) { hit = items[i]; break; } } } if (hit === markActivePrev) return; if (markActivePrev) { try { markActivePrev.classList.remove('active'); } catch (e) {} } markActivePrev = null; var old = _.querySelector('.catalog-item.active'); if (old && old !== hit) { try { old.classList.remove('active'); } catch (e) {} } if (hit) { hit.classList.add('active'); markActivePrev = hit; } } // 打开目录时:把当前章节的高亮项滚动到可视区居中;若它在折叠的“其他章节”分组里先展开 function scrollActiveIntoCenter() { var act = _.querySelector('.catalog-item.active'); if (!act) return; var sp = act.parentElement; while (sp && sp !== _ && !(sp.classList && sp.classList.contains('special-chapters'))) sp = sp.parentElement; if (sp && sp !== _ && sp.style.display === 'none') { sp.style.display = 'block'; var tg = sp.previousElementSibling; if (tg && tg.classList && tg.classList.contains('catalog-toggle')) { tg.textContent = '▲ 其他章节 (' + sp.children.length + ')'; } } var r = act.getBoundingClientRect(), lr = _.getBoundingClientRect(); var cur = r.top - lr.top; var want = (lr.height - r.height) / 2; _.scrollTop += (cur - want); } function openCatalog() { renderCatalog(); n.classList.add('visible'); r.classList.add('visible'); hideBars(); scrollActiveIntoCenter(); 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 + '