// ==UserScript== // @name X阅读模式 // @namespace novel-reader.userscript // @version 1.0.12 // @description 沉浸式小说阅读工具:自动识别正文、自动加载下一章、目录选章、自定义主题/字号/行距,单击上下区域翻页,域名白名单自动开启,从油猴菜单手动启动。 // @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; /* ==================== Readability 正文提取 ==================== */ function isProbablyReaderable(doc, options) { try { return Readability.isProbablyReaderable(doc, options); } catch (e) { return false; } } /* ==================== 常量与状态 ==================== */ var KEY_SETTINGS = 'novel_reader_settings_v1'; var KEY_WHITELIST = 'novel_reader_whitelist_v1'; var MAX_STITCH = 500; var state = { open: false, root: null, win: null, visited: {}, stitchCount: 0, pendingInit: null }; /* ==================== 存储 ==================== */ 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 v = GM_getValue(KEY_WHITELIST, '[]'); var l = JSON.parse(v); return Array.isArray(l) ? l : []; } catch (e) { return []; } } function saveWhitelist(list) { try { GM_setValue(KEY_WHITELIST, JSON.stringify(list)); } catch (e) {} } function inWhitelist(host) { return getWhitelist().indexOf(host) !== -1; } /* ==================== 工具 ==================== */ var CHAP_RE = /(第\s*[0-9零一二三四五六七八九十百千两〇○]+\s*[章节節回卷話话篇集]|chapter\s*\d+|序章|楔子|尾声|后记|番外)/i; var TOC_TEXT_RE = /^(目录|目錄|章节目录|返回目录|查看目录|返回书页|书页|目次|章节列表|列表|目录页)$/; var NAV_WORDS = /^(首页|书架|排行|分类|搜索|登录|注册|完本|最新|书库|书单|我的|消息|反馈|下载|客户端|繁體|繁体|english|返回顶部|上一页|下一页|上一章|下一章|刷新|加入书签|投推荐票|章节错误|字号|夜间模式|手机阅读|电脑版)$/i; 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(); } 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|‹|«|←|< 0 && (!nextBest || ns > nextBest.score)) nextBest = { url: u, score: ns }; var ps = scorePrev(t); if (ps > 0 && (!prevBest || ps > prevBest.score)) prevBest = { url: u, score: ps }; }); var tocUrl = ''; 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) { tocUrl = tu; break; } } } return { url: baseUrl, title: (article && article.title) ? article.title : (doc.title || ''), data: (article && article.content) ? article.content : '', textContent: (article && article.textContent) ? article.textContent : '', next_url: nextBest ? nextBest.url : '', prev_url: prevBest ? prevBest.url : '', tocUrl: tocUrl, toc: [] }; } /* 目录页章节列表提取(含 option 分页链接) */ function extractTocPage(doc, baseUrl) { var host = ''; try { host = new URL(baseUrl).host; } catch (e) {} var anchors = []; try { anchors = Array.prototype.slice.call(doc.querySelectorAll('a[href]')); } catch (e) {} var strict = [], loose = [], seenS = {}, seenL = {}; anchors.forEach(function (a) { var href = a.getAttribute('href'); if (isBadHref(href)) return; var u = resolveUrl(href, baseUrl); if (!u) return; try { if (new URL(u).host !== host) return; } catch (e) { return; } if (noHash(u) === noHash(baseUrl)) return; var t = (a.textContent || '').trim().replace(/\s+/g, ' '); if (t.length < 2 || t.length > 50) return; if (NAV_WORDS.test(t)) return; if (/第\s*\(\d+\/\d+\)\s*页/.test(t)) return; if (CHAP_RE.test(t) && !seenS[u]) { seenS[u] = 1; strict.push({ title: t, url: u }); } if (!seenL[u]) { seenL[u] = 1; loose.push({ title: t, url: u }); } }); var list = strict.length >= 5 ? strict : loose; if (list.length > 50000) list = list.slice(0, 50000); // 提取 option 分页链接(增强) var optionUrls = []; var options = doc.querySelectorAll('option[value]'); options.forEach(function(opt) { var val = opt.getAttribute('value'); if (val) { var u = resolveUrl(val, baseUrl); if (u && !isBadHref(u) && noHash(u) !== noHash(baseUrl)) { if (optionUrls.indexOf(u) === -1) { optionUrls.push(u); } } } }); // 额外:如果 optionUrls 为空,尝试从 select 的 onchange 中提取(可选) if (optionUrls.length === 0) { var selects = doc.querySelectorAll('select[onchange]'); selects.forEach(function(sel) { var onchange = sel.getAttribute('onchange') || ''; var match = onchange.match(/self\.location\.href\s*=\s*options\[selectedIndex\]\.value/); if (match) { // 说明该 select 的 option 的 value 就是URL,已经提取过了,但可能没有 option 被选中? // 这里不再重复 } }); } return { chapters: list, optionUrls: optionUrls }; } /* ==================== 抓取与解析 ==================== */ function fetchUrl(url, cb) { try { 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)); } } 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 toast(msg) { try { var old = document.getElementById('__novel_reader_toast__'); if (old) old.parentNode.removeChild(old); var t = document.createElement('div'); t.id = '__novel_reader_toast__'; t.textContent = msg; t.style.cssText = '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;'; (document.body || document.documentElement).appendChild(t); setTimeout(function () { try { t.parentNode.removeChild(t); } catch (e) {} }, 2400); } catch (e) {} } /* ==================== 阅读界面(iframe + postMessage 桥) ==================== */ 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 16px;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}.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-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}"; 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
18
\n
\n
行距\n
\n
\n
背景\n
\n
\n
\n"; /* ===== iframe 内 JS(已移除左右滑动和音量键) ===== */ var READER_JS = `(function () { 'use strict'; 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'), loadingText = document.getElementById('loadingText'); var STR = { readerMode: '阅读模式', chapters: '目录', noChapters: '暂无目录', loading: '正在加载内容...', loadingJump: '正在加载章节...', loadingNext: '正在加载下一章...', nextChapter: '下一章', loadFail: '章节加载失败', tocLoaded: '已加载目录', night: '夜间', day: '日间' }; document.title = STR.readerMode; loadingText.textContent = STR.loading; // -------- 辅助:数字转换(含“两”) ---------- function chineseToNumber(ch) { var map = { '零':0,'一':1,'二':2,'两':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9,'十':10,'百':100,'千':1000,'万':10000,'亿':100000000 }; 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 clean = title.trim(); var match; match = clean.match(/第([零一二三四五六七八九十百千万亿两]+)[章节回卷话篇集]/i); if (match) return chineseToNumber(match[1]); match = clean.match(/第(\\d+)[章节回卷话篇集]/i); if (match) return parseInt(match[1], 10); 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; } 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' }; /* ================= 桥接 ================= */ function send(msg) { try { window.parent.postMessage(msg, '*'); } catch (err) {} } 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; } applySettings(); tocData = msg.toc || []; tocUrl = msg.tocUrl || ''; btnLoadToc.style.display = tocUrl ? '' : 'none'; if (msg.chapter) { J(msg.chapter, true); I(msg.chapter); } break; case 'chapterReady': y.classList.remove('visible'); if (msg.mode === 'jump') { hideLoading(); resetAndLoad(msg.chapter); } else { if (awaitingNextUrl && msg.url === awaitingNextUrl) { awaitingNextUrl = ''; stitching = false; J(msg.chapter, false); stitchFails = 0; setTimeout(checkAutoLoad, 0); } } break; case 'chapterFailed': y.classList.remove('visible'); if (msg.mode === 'jump') { hideLoading(); toast(STR.loadFail); } else { stitching = false; awaitingNextUrl = ''; stitchFails++; if (stitchFails < 3) setTimeout(checkAutoLoad, 2000); } break; case 'tocReady': tocData = msg.toc || []; tocLoading = false; btnLoadToc.textContent = '加载目录页'; btnLoadToc.style.opacity = ''; btnLoadToc.style.pointerEvents = ''; btnLoadToc.style.cursor = ''; renderCatalog(); break; } } /* ================= 章节渲染 ================= */ function sanitize(el) { var bad = el.querySelectorAll('script,iframe,object,embed,link,meta,form,input,button,select,textarea'); for (var i = 0; i < bad.length; i++) { try { bad[i].parentNode.removeChild(bad[i]); } catch (e2) {} } } function J(t, first) { if (!t || !t.url) return; var block = document.createElement('div'); block.className = 'chapter-block'; block.dataset.url = t.url || ''; if (!first) { var divider = document.createElement('div'); divider.className = 'chapter-divider'; var dt = document.createElement('span'); dt.className = 'chapter-divider-title'; dt.textContent = t.title || STR.nextChapter; divider.appendChild(dt); block.appendChild(divider); } var titleEl = document.createElement('div'); titleEl.className = 'chapter-title'; titleEl.textContent = t.title || ''; titleEl.style.fontSize = (s.fontSize + 4) + 'px'; block.appendChild(titleEl); var contentEl = document.createElement('div'); contentEl.className = 'chapter-content'; contentEl.style.fontSize = s.fontSize + 'px'; contentEl.style.lineHeight = s.lineHeight; contentEl.innerHTML = t.data || ''; sanitize(contentEl); block.appendChild(contentEl); f.appendChild(block); C.push({ url: t.url, title: t.title || '', nextUrl: t.next_url || '', element: block }); var imgs = block.querySelectorAll('img[data-src]'); for (var i = 0; i < imgs.length; i++) { (function (img) { var src = img.getAttribute('data-src'); if (!src) return; var im = new Image(); im.onload = function () { img.src = src; img.removeAttribute('data-src'); }; im.onerror = function () { img.removeAttribute('data-src'); }; im.src = src; })(imgs[i]); } if (first) { d = 0; v.textContent = t.title || STR.readerMode; p.style.display = 'none'; g.style.display = 'block'; E.scrollTop = 0; send({ type: 'setCurrentUrl', url: t.url || '' }); } renderCatalog(); } function I(t) { if (t && t.next_url) M(t.url, t.next_url); } function findLoadedByUrl(url) { if (!url) return -1; for (var i = 0; i < C.length; i++) if (C[i].url === url) return i; return -1; } function M(fromUrl, nextUrl) { if (!nextUrl || stitching) return; if (findLoadedByUrl(nextUrl) !== -1) return; stitching = true; awaitingNextUrl = nextUrl; y.classList.add('visible'); send({ type: 'fetchChapter', url: nextUrl, from: fromUrl, mode: 'stitch' }); } function resetAndLoad(chapter) { C = []; d = 0; f.innerHTML = ''; stitching = false; awaitingNextUrl = ''; stitchFails = 0; y.classList.remove('visible'); J(chapter, true); I(chapter); E.scrollTop = 0; } function 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 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 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; }); function createItem(it, isActive) { var el = document.createElement('div'); el.className = 'catalog-item' + (isActive ? ' active' : ''); if (it.url) el.dataset.tocUrl = it.url; 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; markActive(); } } else if (it.url) { showLoading(STR.loadingJump); send({ type: 'fetchChapter', url: it.url, mode: 'jump' }); } }); return el; } var frag = document.createDocumentFragment(); var cur = currentUrl(); normal.forEach(function(o) { var isActive = o.item.url === cur; frag.appendChild(createItem(o.item, isActive)); }); 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(e) { e.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) { var isActive = it.url === cur; specialContainer.appendChild(createItem(it, isActive)); }); frag.appendChild(specialContainer); } _.appendChild(frag); markActive(); } var markActivePrev = null; function markActive() { var cur = currentUrl(); if (markActivePrev && markActivePrev.dataset && markActivePrev.dataset.tocUrl === cur) return; if (markActivePrev) { try { markActivePrev.classList.remove('active'); } catch (e) {} markActivePrev = null; } var el = _.querySelector('.catalog-item.active'); if (el) { try { el.classList.remove('active'); } catch (e) {} } if (!cur) return; var hit = _.querySelector('.catalog-item[data-toc-url="' + cur.replace(/"/g, '%22') + '"]'); if (hit) { hit.classList.add('active'); markActivePrev = hit; } } function openCatalog() { renderCatalog(); n.classList.add('visible'); r.classList.add('visible'); o = false; m.classList.remove('visible'); h.classList.remove('visible'); e.classList.remove('visible'); var act = _.querySelector('.catalog-item.active'); if (act) { try { act.scrollIntoView({ block: 'center' }); } catch (e2) {} } if (tocData.length === 0 && tocUrl && !tocLoading) { tocLoading = true; if (btnLoadToc.style.display !== 'none') { btnLoadToc.textContent = '加载中...'; btnLoadToc.style.opacity = '0.5'; btnLoadToc.style.pointerEvents = 'none'; } send({ type: 'fetchToc', url: tocUrl }); } } 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); } } var lastScrollTop = 0; var scrollTicking = false; 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; send({ type: 'setCurrentUrl', url: C[i].url }); 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; } 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) return; tocLoading = true; btnLoadToc.textContent = '加载中...'; btnLoadToc.style.opacity = '0.5'; btnLoadToc.style.pointerEvents = 'none'; btnLoadToc.style.cursor = 'default'; send({ type: 'fetchToc', url: tocUrl }); }); /* 夜间模式 */ var btnNight = document.getElementById('btnNightMode'); btnNight.addEventListener('click', function (ev) { ev.stopPropagation(); if (s.theme === 'dark') { s.theme = s.prevTheme || 'warm'; btnNight.querySelector('span').textContent = STR.night; btnNight.classList.remove('active'); } else { s.prevTheme = s.theme; s.theme = 'dark'; btnNight.querySelector('span').textContent = STR.day; btnNight.classList.add('active'); } document.body.className = 'theme-' + s.theme; syncThemeDots(); saveSettings(); }); /* ================= 设置面板 ================= */ var Y = document.getElementById('settingsPanel'), j = document.getElementById('settingsMask'); function openSettings() { G = true; Y.classList.add('visible'); j.classList.add('visible'); o = false; m.classList.remove('visible'); h.classList.remove('visible'); e.classList.remove('visible'); } 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(); }); 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 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(); if (s.theme === 'dark') { btnNight.querySelector('span').textContent = STR.day; btnNight.classList.add('active'); } var cs = f.querySelectorAll('.chapter-content'); for (var k = 0; k < cs.length; k++) cs[k].style.lineHeight = s.lineHeight; } 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 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') { btnNight.querySelector('span').textContent = STR.day; btnNight.classList.add('active'); } else { s.prevTheme = s.theme; btnNight.querySelector('span').textContent = STR.night; btnNight.classList.remove('active'); } saveSettings(); }); /* ================= 提示 ================= */ function toast(msg) { try { var old = document.getElementById('__reader_toast__'); if (old) old.parentNode.removeChild(old); var t = document.createElement('div'); t.id = '__reader_toast__'; t.textContent = msg; t.style.cssText = '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;'; document.body.appendChild(t); setTimeout(function () { try { t.parentNode.removeChild(t); } catch (e2) {} }, 2200); } catch (e2) {} } send({ type: 'ready' }); })(); `; var READER_HTML = '' + '' + '阅读模式' + '' + READER_BODY + '