// ==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|‹|«|←|<)$/i.test(t)) return 1; return 0; } /* ==================== 内容提取 ==================== */ function extractFromDoc(doc, baseUrl) { var article = null; try { var clone = doc.cloneNode(true); article = new Readability(clone, { charThreshold: 100, keepClasses: false }).parse(); } catch (e) { article = null; } var anchors = []; try { anchors = Array.prototype.slice.call(doc.querySelectorAll('a[href]')); } catch (e) {} var selfUrl = noHash(baseUrl); var host = ''; try { host = new URL(baseUrl).host; } catch (e) {} function relLink(rel) { try { var a = doc.querySelector('a[rel="' + rel + '"]'); if (a) { var href = a.getAttribute('href'); if (!isBadHref(href)) { var u = resolveUrl(href, baseUrl); if (u && noHash(u) !== selfUrl) return u; } } } catch (e) {} return null; } var rn = relLink('next'); var rp = relLink('prev'); var nextBest = rn ? { url: rn, score: 3 } : null; var prevBest = rp ? { url: rp, 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 }; 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