// ==UserScript== // @name 同人小说网 trxs.cc 阅读增强(阅读模式 · 夜间 · 去广告) // @namespace https://github.com/zzyoxml/trxs-reader // @version 1.3.0 // @description 为 trxs.cc 提供沉浸式阅读模式、夜间/护眼配色、广告清理、目录跳转、自动连读、阅读进度记忆 // @author zzyoxml // @license MIT // @homepageURL https://github.com/zzyoxml/trxs-reader // @supportURL https://github.com/zzyoxml/trxs-reader/issues // @match *://trxs.cc/* // @match *://www.trxs.cc/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @connect trxs.cc // @connect www.trxs.cc // @run-at document-start // @noframes // ==/UserScript== (function () { 'use strict'; /* ========================================================================= * 0. 站点判定 * ========================================================================= */ if (window.top !== window.self) return; if (!/^(www\.)?trxs\.cc$/i.test(location.hostname)) return; const CHAP_RE = /^\/tongren\/(\d+)\/(\d+)\.html$/i; const BOOK_RE = /^\/tongren\/(\d+)\.html$/i; const TOC_LINK_RE = /^\/tongren\/(\d+)\/(\d+)\.html$/i; /* ========================================================================= * 1. 广告拦截(document-start 阶段先行) * ========================================================================= * trxs.cc 的广告并不在自己的 HTML 里,而是由以下三方脚本动态注入: * /skin/default/js/funNav.js -> cpt.geniee.jp + securepubads.g.doubleclick.net * /skin/default/js/tongji.js -> googletagmanager.com (GA) * 因此策略是:拦截脚本注入 + 清理已出现的广告节点 + 隐藏广告位容器。 * 注意:页面末尾 Cloudflare 注入的那个 1px 隐藏 iframe 不是广告,不能误删。 */ const AD_HOSTS = [ 'cpt.geniee.jp', 'geniee.jp', 'securepubads.g.doubleclick.net', 'doubleclick.net', 'googletagmanager.com', 'google-analytics.com', 'googlesyndication.com', 'googleadservices.com', 'adservice.google.com', 'adsbygoogle.js', 'pubmatic.com', 'taboola.com', 'outbrain.com' ]; const AD_SELECTORS = [ 'script[src*="geniee"]', 'script[src*="doubleclick"]', 'script[src*="googletagmanager"]', 'script[src*="google-analytics"]', 'script[src*="googlesyndication"]', 'script[src*="adsbygoogle"]', 'iframe[src*="geniee"]', 'iframe[src*="doubleclick"]', 'iframe[src*="googlesyndication"]', 'iframe[src*="googleads"]', 'iframe[src*="googletagmanager"]', 'ins.adsbygoogle', '.adsbygoogle', '[id^="div-gpt-ad"]', '[id*="geniee"]', '[id^="google_ads"]', '[class*="gnshb"]' ]; const isAdUrl = (text) => { if (typeof text !== 'string' || !text) return false; const s = text.toLowerCase(); for (let i = 0; i < AD_HOSTS.length; i++) { if (s.indexOf(AD_HOSTS[i]) >= 0) return true; } return false; }; // 1.1 拦截 document.write 写入的广告脚本(funNav.js / tongji.js 都靠它) (function hookDocumentWrite() { try { const rawWrite = document.write; const rawWriteln = document.writeln; document.write = function () { for (let i = 0; i < arguments.length; i++) { if (isAdUrl(arguments[i])) return undefined; } return rawWrite.apply(document, arguments); }; document.writeln = function () { for (let i = 0; i < arguments.length; i++) { if (isAdUrl(arguments[i])) return undefined; } return rawWriteln.apply(document, arguments); }; } catch (e) { /* 某些环境下 document.write 只读,忽略 */ } })(); function sweepAds(root) { if (!root || !root.querySelectorAll) return; for (let i = 0; i < AD_SELECTORS.length; i++) { let nodes; try { nodes = root.querySelectorAll(AD_SELECTORS[i]); } catch (e) { continue; } for (let j = nodes.length - 1; j >= 0; j--) { const n = nodes[j]; if (n && n.parentNode) n.parentNode.removeChild(n); } } } function watchAds() { sweepAds(document); if (typeof MutationObserver !== 'function' || !document.documentElement) return; const mo = new MutationObserver((records) => { for (let i = 0; i < records.length; i++) { const added = records[i].addedNodes; for (let j = 0; j < added.length; j++) { const node = added[j]; if (node.nodeType !== 1) continue; if (node.tagName === 'SCRIPT' || node.tagName === 'IFRAME') { const src = node.getAttribute('src'); if (src && isAdUrl(src)) { node.remove(); continue; } if (node.tagName === 'SCRIPT' && isAdUrl(node.textContent)) { node.remove(); continue; } } if (node.querySelectorAll) sweepAds(node); } } }); mo.observe(document.documentElement, { childList: true, subtree: true }); } /* ========================================================================= * 2. 本地设置 / 进度存储 * ========================================================================= */ const DEFAULTS = { fontSize: 19, lineHeight: 1.9, maxWidth: 780, family: 'system', theme: 'light', autoNext: true, autoScroll: false, // 自动滚动 scrollSpeed: 4, // 自动滚动速度 1-10 darkNative: false // 原页面夜间模式 }; /* ------------------------------------------------------------------------- * 固定行为(不提供开关) * ------------------------------------------------------------------------- * 去广告 / 隐藏推荐 / 隐藏登录条 / 显示悬浮按钮 这四项是脚本的既有价值, * 做成 checkbox 反而让人以为可以关掉,实测也没人会关。因此直接固化, * 且不受历史存档里的旧值影响(老用户存过 noAds:false 也不会被尊重)。 * ----------------------------------------------------------------------- */ const ALWAYS = { noAds: true, noHot: true, noLogin: true, showFab: true }; function readStore(key, def) { try { const v = GM_getValue(key, null); if (v === null || v === undefined) return def; if (typeof v === 'string') { try { return JSON.parse(v); } catch (e) { return def; } } return v; } catch (e) { return def; } } function writeStore(key, value) { try { GM_setValue(key, value); } catch (e) { /* ignore */ } } let settings = Object.assign({}, DEFAULTS, readStore('trxs:settings', {}) || {}); function saveSettings() { writeStore('trxs:settings', settings); } const FAMILIES = { system: '-apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif', yahei: '"Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif', song: '"Songti SC", "SimSun", "Source Han Serif SC", "Noto Serif CJK SC", serif', kai: '"Kaiti SC", "KaiTi", "STKaiti", serif', hei: '"Microsoft YaHei", "PingFang SC", "Heiti SC", sans-serif' }; const THEMES = [ { id: 'light', name: '日间' }, { id: 'sepia', name: '羊皮纸' }, { id: 'green', name: '护眼' }, { id: 'dark', name: '夜间' }, { id: 'black', name: '纯黑' } ]; /* ========================================================================= * 3. 通用工具 * ========================================================================= */ const $ = (sel, root) => (root || document).querySelector(sel); const $$ = (sel, root) => Array.prototype.slice.call((root || document).querySelectorAll(sel)); let toastTimer = null; function toast(msg, ms) { if (!document.body) return; let el = $('#trx-toast'); if (!el) { el = document.createElement('div'); el.id = 'trx-toast'; document.body.appendChild(el); } el.textContent = msg; el.classList.add('trx-show'); clearTimeout(toastTimer); toastTimer = setTimeout(function () { el.classList.remove('trx-show'); }, ms || 2000); } function sniffCharset(bytes) { const head = new TextDecoder('latin1').decode(bytes.subarray(0, 2048)); const m = /charset\s*=\s*["']?([a-zA-Z0-9_\-]+)/i.exec(head); if (m) { const cs = m[1].toLowerCase(); if (cs === 'utf-8' || cs === 'utf8') return 'utf-8'; if (cs === 'gbk' || cs === 'gb2312' || cs === 'gb18030') return 'gbk'; return cs; } return 'gbk'; // trxs.cc 默认 gb2312 } function decodeBytes(bytes) { let declared = 'gbk'; try { declared = sniffCharset(bytes); } catch (e) { declared = 'gbk'; } // 页面声明的 charset 优先 if (declared === 'utf-8') { try { return new TextDecoder('utf-8').decode(bytes); } catch (e) { /* fallthrough */ } } // GBK 的高位字节几乎不可能通过严格 UTF-8 校验, // 因此用「严格 UTF-8 是否成立」来识别被错误标注为 gb2312 的页面。 let gbkText = null; try { gbkText = new TextDecoder('gbk').decode(bytes); } catch (e) { gbkText = null; } try { const utf8Text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); if (utf8Text) return utf8Text; } catch (e) { /* 不是合法 UTF-8,继续用 gbk */ } if (gbkText !== null) return gbkText; try { return new TextDecoder('utf-8').decode(bytes); } catch (e2) { return new TextDecoder('gbk', { fatal: false }).decode(bytes); } } /** 站内页面统一用 GB2312/GBK,必须自行解码,不能直接依赖 responseText。 */ function gmFetch(url) { return new Promise(function (resolve, reject) { const handle = function (raw) { try { let u8 = null; if (raw && typeof raw.byteLength === 'number') { if (ArrayBuffer.isView(raw)) { u8 = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength); } else { u8 = new Uint8Array(raw); } } const text = u8 ? decodeBytes(u8) : null; if (!text) reject(new Error('空响应')); else resolve(text); } catch (e) { reject(e); } }; if (typeof GM_xmlhttpRequest === 'function') { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', timeout: 25000, onload: function (res) { handle(res.response); }, onerror: function () { reject(new Error('网络错误')); }, ontimeout: function () { reject(new Error('请求超时')); } }); } else { // 没有 GM_xmlhttpRequest 时退化为 fetch fetch(url, { credentials: 'include' }) .then(function (r) { return r.arrayBuffer(); }) .then(handle) .catch(reject); } }); } function parseDoc(html) { try { return new DOMParser().parseFromString(html, 'text/html'); } catch (e) { return null; } } const chapUrl = (bookId, n) => location.origin + '/tongren/' + bookId + '/' + n + '.html'; const bookUrl = (bookId) => location.origin + '/tongren/' + bookId + '.html'; /* ========================================================================= * 4. 正文解析(针对 trxs.cc 的固定 DOM 结构) * ========================================================================= * 章节页结构: * .readDetail > .read_chapterName > h1(书名 第N章) + .textinfo > em(作者:xx) * .readDetail > .read_chapterDetail > 大量
(正文,段首为两个全角空格) * .pageNav : 首章 / 上一章 / {n}/{total} / 下一章 / 尾章 / 目录 * 「上一篇 / 下一篇」在 .next_pre 中,不在正文容器内,额外做一次保险过滤。 */ const NAV_TAIL = /^(上一篇|下一篇|上一章|下一章|返回目录|返回列表|目录|首章|尾章|加入书签)\s*[::]?$/; function makeParagraph(text, extraClass) { const p = document.createElement('p'); p.className = 'trx-p' + (extraClass ? ' ' + extraClass : ''); p.textContent = text; return p; } function extractBody(detailEl) { const out = []; if (!detailEl) return out; const pushText = function (raw) { if (raw === null || raw === undefined) return; let text = String(raw).replace(/\u00a0/g, ' '); // 去掉段首的全角空格(改用 CSS text-indent 统一缩进) text = text.replace(/^[\s\u3000]+/, '').replace(/[\s\u3000]+$/, ''); if (!text) return; if (NAV_TAIL.test(text)) return; if (/^(全本小说网|请记住本站|手机用户请|本章未完)/.test(text)) return; out.push(makeParagraph(text)); }; const pushImage = function (img) { const p = document.createElement('p'); p.className = 'trx-p trx-img'; const clone = img.cloneNode(false); clone.removeAttribute('width'); clone.removeAttribute('height'); clone.removeAttribute('style'); if (clone.getAttribute('src')) { p.appendChild(clone); out.push(p); } }; const pushBlank = function () { if (!out.length) return; const last = out[out.length - 1]; if (last && last.classList.contains('trx-blank')) return; out.push(makeParagraph('', 'trx-blank')); }; const walk = function (node) { const kids = Array.prototype.slice.call(node.childNodes); for (let i = 0; i < kids.length; i++) { const child = kids[i]; if (child.nodeType === 3) { pushText(child.nodeValue); continue; } if (child.nodeType !== 1) continue; const tag = child.tagName.toUpperCase(); if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'INS' || tag === 'IFRAME' || tag === 'NOSCRIPT') continue; if (tag === 'IMG') { pushImage(child); continue; } if (tag === 'BR') { pushBlank(); continue; } if (tag === 'P' || tag === 'DIV' || tag === 'SECTION' || tag === 'LI' || tag === 'BLOCKQUOTE') { // 段落内部若还有块级节点,继续递归;否则整体取文本 if (child.querySelector && child.querySelector('p, img, br')) { walk(child); } else { pushText(child.textContent); } continue; } // span / a / em / strong 等行内节点 pushText(child.textContent); } }; walk(detailEl); // 去掉尾部多余空段 while (out.length && out[out.length - 1].classList.contains('trx-blank')) out.pop(); return out; } function parseChapter(html, url) { const doc = parseDoc(html); if (!doc) return null; const detail = doc.querySelector('.read_chapterDetail'); if (!detail) return null; const href = url || ''; const urlMatch = /\/tongren\/(\d+)\/(\d+)\.html/i.exec(href); const bookId = urlMatch ? urlMatch[1] : null; const urlIndex = urlMatch ? parseInt(urlMatch[2], 10) : null; const navEl = doc.querySelector('.pageNav'); const navText = navEl ? navEl.textContent.replace(/\s+/g, ' ') : ''; const navMatch = /(\d+)\s*\/\s*(\d+)/.exec(navText); const index = navMatch ? parseInt(navMatch[1], 10) : urlIndex; const total = navMatch ? parseInt(navMatch[2], 10) : null; const h1 = doc.querySelector('.read_chapterName h1') || doc.querySelector('.read_chapterName h2') || doc.querySelector('h1'); const title = h1 ? h1.textContent.replace(/\s+/g, ' ').trim() : ''; const em = doc.querySelector('.textinfo em'); const author = em ? em.textContent.replace(/^作者[::]\s*/, '').trim() : ''; // 「书名 第N章」→ 书名 let bookTitle = title.replace(/\s*第\s*[0-9零一二三四五六七八九十百千万两]+\s*[章节回卷].*$/, '').trim(); if (!bookTitle) bookTitle = title; const nodes = extractBody(detail); if (!nodes.length) return null; return { bookId: bookId, index: index, total: total, title: title, bookTitle: bookTitle, author: author, nodes: nodes }; } function shortTitle(data) { let t = data.title || ''; if (data.bookTitle && t.indexOf(data.bookTitle) === 0) { t = t.slice(data.bookTitle.length).trim(); } return t || ('第 ' + data.index + ' 章'); } /* ========================================================================= * 5. 样式 * ========================================================================= */ const CSS = ` /* ---------- 原页面:去广告 / 去杂物 ---------- */ html.trxs-noads .membershow, html.trxs-noads .dp_head, html.trxs-noads #onclickshowdiv, html.trxs-noads .star_box, html.trxs-noads ins.adsbygoogle, html.trxs-noads .adsbygoogle { display: none !important; } html.trxs-nohot .hotlist, html.trxs-nohot .next_pre { display: none !important; } html.trxs-nologin .membershow { display: none !important; } /* Cloudflare 的 1px 隐藏 iframe 保留,不动 */ /* ---------- 原页面:夜间模式 ---------- */ html.trxs-dark { background: #15161a !important; } html.trxs-dark body, html.trxs-dark .reading { background: #15161a !important; color: #c2c4cc !important; } html.trxs-dark .head, html.trxs-dark .newNav, html.trxs-dark #newNav, html.trxs-dark .readContent, html.trxs-dark #readContent_set, html.trxs-dark .readDetail, html.trxs-dark .topReadContent, html.trxs-dark .book_list, html.trxs-dark .book_info, html.trxs-dark .lists, html.trxs-dark .books, html.trxs-dark .booknews, html.trxs-dark .container, html.trxs-dark .box, html.trxs-dark .copyright, html.trxs-dark .footer, html.trxs-dark .bk { background: #1b1d22 !important; border-color: #2b2e37 !important; box-shadow: none !important; } html.trxs-dark .head, html.trxs-dark .navTop { border-bottom: 2px solid #0aa89a !important; } html.trxs-dark .readTop, html.trxs-dark .menNav { background: #1b1d22 !important; border-bottom-color: #2b2e37 !important; } html.trxs-dark .readTop, html.trxs-dark .readTop a { color: #8b8f99 !important; } html.trxs-dark .read_chapterName h1, html.trxs-dark .read_chapterName h2 { color: #d8dae1 !important; } html.trxs-dark .textinfo, html.trxs-dark .textinfo em, html.trxs-dark .textinfo a, html.trxs-dark .textinfo span { color: #8b8f99 !important; } html.trxs-dark .read_chapterDetail, html.trxs-dark .read_chapterDetail p { color: #c6c8d0 !important; } html.trxs-dark .read_chapterDetail img { opacity: .88; } html.trxs-dark .pageNav, html.trxs-dark .pageNav a { color: #9aa0aa !important; } html.trxs-dark a, html.trxs-dark .littleLogo { color: #b9bcc5 !important; } html.trxs-dark a:hover { color: #2fd0bd !important; } html.trxs-dark .book_info h1, html.trxs-dark .book_info p, html.trxs-dark .infos .date, html.trxs-dark .infos .date a, html.trxs-dark .infos span, html.trxs-dark .hotlist h3, html.trxs-dark .hotlist ul li a { color: #b9bcc5 !important; } html.trxs-dark .book_list ul li { background-image: none !important; } html.trxs-dark .book_list ul li a { color: #b9bcc5 !important; } html.trxs-dark .book_list ul li a:visited { color: #7f8798 !important; } html.trxs-dark .search .search-keyword, html.trxs-dark .search .search-submit { background: #232630 !important; color: #c2c4cc !important; border-color: #343845 !important; } html.trxs-dark .booktips h3 a, html.trxs-dark .infos h3 a { background: #33384a !important; color: #d8dae1 !important; } html.trxs-dark .copyright, html.trxs-dark .copyright a { color: #6f747f !important; } /* ---------- 右下角悬浮按钮 ---------- */ #trx-fab { position: fixed; right: 18px; bottom: 18px; z-index: 2147482000; display: flex; flex-direction: column; gap: 8px; align-items: flex-end; font: 13px/1.4 "Microsoft YaHei", sans-serif; } #trx-fab button { display: flex; align-items: center; gap: 6px; padding: 9px 14px; border: none; border-radius: 20px; cursor: pointer; color: #fff; background: rgba(24, 26, 32, .82); backdrop-filter: blur(6px); box-shadow: 0 4px 14px rgba(0, 0, 0, .28); transition: transform .15s, background .15s; white-space: nowrap; } #trx-fab button:hover { transform: translateY(-2px); background: #0aa89a; } #trx-fab .trx-fab-main { background: #0aa89a; font-weight: bold; } html.trx-open #trx-fab { display: none !important; } /* ---------- 阅读器 ---------- */ html.trx-open, html.trx-open body { overflow: hidden !important; height: 100% !important; } #trx-reader { --trx-bg: #fbfaf7; --trx-fg: #2b2d33; --trx-dim: #8b8d94; --trx-panel: #ffffff; --trx-line: #e6e4dd; --trx-accent: #0aa89a; --trx-fs: 19px; --trx-lh: 1.9; --trx-w: 780px; position: fixed; inset: 0; z-index: 2147483000; display: flex; flex-direction: column; background: var(--trx-bg); color: var(--trx-fg); font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif; -webkit-font-smoothing: antialiased; } #trx-reader[data-theme="sepia"] { --trx-bg: #f5eddd; --trx-fg: #4a3d31; --trx-panel: #efe4cf; --trx-line: #e0d3ba; --trx-dim: #9b8b74; --trx-accent: #b07b3a; } #trx-reader[data-theme="green"] { --trx-bg: #dbe8d3; --trx-fg: #2e3a2b; --trx-panel: #e7f1e0; --trx-line: #c3d5b9; --trx-dim: #78866f; --trx-accent: #4c8b3f; } #trx-reader[data-theme="dark"] { --trx-bg: #191a1e; --trx-fg: #c8cbd3; --trx-panel: #212329; --trx-line: #2e313a; --trx-dim: #838894; --trx-accent: #2fd0bd; } #trx-reader[data-theme="black"] { --trx-bg: #000000; --trx-fg: #9b9b9b; --trx-panel: #101010; --trx-line: #232323; --trx-dim: #6f6f6f; --trx-accent: #2fd0bd; } #trx-bar { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; height: 48px; padding: 0 12px; background: var(--trx-panel); border-bottom: 1px solid var(--trx-line); font-size: 13px; color: var(--trx-dim); } #trx-bar-title { flex: 1 1 auto; min-width: 0; display: flex; align-items: baseline; gap: 10px; overflow: hidden; } #trx-bar-title .trx-book { color: var(--trx-fg); font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 46%; } #trx-bar-title .trx-chap { color: var(--trx-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #trx-bar button { flex: 0 0 auto; height: 30px; min-width: 30px; padding: 0 9px; border: 1px solid var(--trx-line); border-radius: 6px; background: transparent; color: var(--trx-fg); cursor: pointer; font-size: 13px; line-height: 28px; white-space: nowrap; } #trx-bar button:hover { border-color: var(--trx-accent); color: var(--trx-accent); } #trx-bar button.trx-on { border-color: var(--trx-accent); color: var(--trx-accent); background: color-mix(in srgb, var(--trx-accent) 14%, transparent); } #trx-bar .trx-bar-actions { display: flex; gap: 8px; } #trx-progress { flex: 0 0 auto; height: 3px; background: transparent; } #trx-progress-bar { height: 3px; width: 0; background: var(--trx-accent); transition: width .1s linear; } #trx-main { flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; scroll-behavior: smooth; } /* 自动滚动开启时关掉平滑滚动,否则每帧赋 scrollTop 会被平滑动画吞掉 */ #trx-reader.trx-autoscroll #trx-main { scroll-behavior: auto; } #trx-content { max-width: var(--trx-w); margin: 0 auto; padding: 26px 22px 140px; font-size: var(--trx-fs); line-height: var(--trx-lh); } .trx-chap { margin-bottom: 2.2em; } .trx-chap + .trx-chap { border-top: 1px dashed var(--trx-line); padding-top: 1.6em; } .trx-chap-title { font-size: calc(var(--trx-fs) * 1.02); font-weight: 600; color: var(--trx-fg); text-align: center; margin: 0 0 1.4em; padding: 0; letter-spacing: .02em; } .trx-chap-title .trx-chap-sub { display: block; font-size: .62em; font-weight: 400; color: var(--trx-dim); margin-top: .5em; } .trx-p { margin: 0 0 .82em; text-indent: 2em; word-break: break-word; overflow-wrap: break-word; } .trx-p.trx-blank { height: .6em; margin: 0; } .trx-p.trx-img { text-indent: 0; text-align: center; margin: 1.2em 0; } .trx-p.trx-img img { max-width: 100%; height: auto; border-radius: 3px; } .trx-loading { text-align: center; padding: 14px 0 30px; color: var(--trx-dim); font-size: 13px; } #trx-pager { position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 2147483200; display: flex; align-items: center; gap: 10px; padding: 7px 12px; border-radius: 22px; background: color-mix(in srgb, var(--trx-panel) 88%, transparent); border: 1px solid var(--trx-line); box-shadow: 0 4px 16px rgba(0,0,0,.16); font-size: 12px; color: var(--trx-dim); white-space: nowrap; backdrop-filter: blur(8px); } #trx-pager button { height: 28px; padding: 0 12px; border-radius: 14px; border: 1px solid var(--trx-line); background: transparent; color: var(--trx-fg); font-size: 12px; cursor: pointer; } #trx-pager button:hover { border-color: var(--trx-accent); color: var(--trx-accent); } #trx-pager button:disabled { opacity: .38; cursor: not-allowed; } /* 目录侧栏 */ #trx-toc { position: fixed; top: 0; left: 0; bottom: 0; width: 320px; max-width: 86vw; z-index: 2147483400; display: flex; flex-direction: column; background: var(--trx-panel); border-right: 1px solid var(--trx-line); transform: translateX(-102%); transition: transform .2s ease; box-shadow: 6px 0 24px rgba(0,0,0,.16); } #trx-toc.trx-open { transform: translateX(0); } .trx-panel-head { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; height: 46px; padding: 0 12px; border-bottom: 1px solid var(--trx-line); font-size: 13px; color: var(--trx-fg); } .trx-panel-head .trx-panel-title { font-weight: 600; } .trx-panel-head .trx-panel-close { margin-left: auto; width: 28px; height: 28px; line-height: 26px; border: 1px solid var(--trx-line); border-radius: 6px; background: transparent; color: var(--trx-fg); cursor: pointer; text-align: center; } #trx-toc-search { flex: 1 1 auto; min-width: 0; height: 28px; padding: 0 8px; border: 1px solid var(--trx-line); border-radius: 6px; background: transparent; color: var(--trx-fg); font-size: 12px; outline: none; } #trx-toc-list { flex: 1 1 auto; overflow-y: auto; padding: 6px 0 18px; } #trx-toc-list a { display: flex; align-items: center; gap: 8px; padding: 9px 14px; font-size: 13px; color: var(--trx-fg); text-decoration: none; border-left: 3px solid transparent; } #trx-toc-list a:hover { background: rgba(10, 168, 154, .08); } #trx-toc-list a.trx-cur { border-left-color: var(--trx-accent); background: rgba(10, 168, 154, .12); font-weight: 600; } #trx-toc-list a .trx-toc-n { color: var(--trx-dim); font-size: 12px; min-width: 42px; } #trx-toc-mask { position: fixed; inset: 0; z-index: 2147483300; background: rgba(0,0,0,.36); opacity: 0; pointer-events: none; transition: opacity .2s; } /* 设置面板 */ #trx-set { position: fixed; right: 0; bottom: 0; width: 340px; max-width: 92vw; z-index: 2147483400; background: var(--trx-panel); border-left: 1px solid var(--trx-line); border-top: 1px solid var(--trx-line); border-radius: 12px 0 0 0; box-shadow: -6px -6px 26px rgba(0,0,0,.18); transform: translateY(102%); transition: transform .22s ease; padding-bottom: 10px; max-height: 78vh; overflow-y: auto; } #trx-set.trx-open { transform: translateY(0); } #trx-set .trx-row { display: flex; align-items: center; gap: 10px; padding: 10px 16px; font-size: 13px; color: var(--trx-fg); } #trx-set .trx-row label { flex: 0 0 auto; width: 62px; color: var(--trx-dim); } #trx-set .trx-row input[type="range"] { flex: 1 1 auto; accent-color: var(--trx-accent); } #trx-set .trx-row .trx-val { flex: 0 0 auto; width: 42px; text-align: right; color: var(--trx-dim); font-variant-numeric: tabular-nums; } #trx-set .trx-row select, #trx-set .trx-row input[type="checkbox"] { accent-color: var(--trx-accent); } #trx-set .trx-row select { flex: 1 1 auto; height: 28px; padding: 0 6px; border: 1px solid var(--trx-line); border-radius: 6px; background: transparent; color: var(--trx-fg); font-size: 13px; } #trx-set .trx-themes { flex: 1 1 auto; display: flex; gap: 6px; flex-wrap: wrap; } #trx-set .trx-theme-btn { flex: 1 1 auto; min-width: 54px; height: 30px; border-radius: 6px; cursor: pointer; border: 1px solid var(--trx-line); font-size: 12px; } #trx-set .trx-theme-btn.trx-on { outline: 2px solid var(--trx-accent); outline-offset: -1px; font-weight: 600; } #trx-set .trx-sheet-foot { display: flex; gap: 10px; padding: 4px 16px 0; } #trx-set .trx-sheet-foot button { flex: 1 1 0; height: 32px; border-radius: 6px; cursor: pointer; border: 1px solid var(--trx-line); background: transparent; color: var(--trx-fg); font-size: 13px; } /* 提示 */ #trx-toast { position: fixed; left: 50%; bottom: 84px; transform: translate(-50%, 10px); z-index: 2147483600; padding: 9px 16px; border-radius: 8px; background: rgba(20, 22, 28, .92); color: #fff; font: 13px/1.5 "Microsoft YaHei", sans-serif; opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; max-width: 80vw; text-align: center; } #trx-toast.trx-show { opacity: 1; transform: translate(-50%, 0); } @media (max-width: 720px) { #trx-content { padding: 18px 16px 130px; } #trx-bar { height: 44px; gap: 6px; } #trx-bar-title .trx-book { display: none; } #trx-pager { bottom: 10px; padding: 6px 8px; } #trx-fab button { padding: 8px 12px; font-size: 12px; } } `; function injectCSS() { if (typeof GM_addStyle === 'function') { GM_addStyle(CSS); } else { const st = document.createElement('style'); st.textContent = CSS; (document.head || document.documentElement).appendChild(st); } } /* ========================================================================= * 6. 原页面标记应用(广告 / 夜间) * ========================================================================= */ function applyNativeFlags() { const root = document.documentElement; if (!root) return; root.classList.toggle('trxs-noads', ALWAYS.noAds); root.classList.toggle('trxs-nohot', ALWAYS.noHot); root.classList.toggle('trxs-nologin', ALWAYS.noLogin && ALWAYS.noAds); root.classList.toggle('trxs-dark', !!settings.darkNative); sweepAds(document); updateFab(); } function toggleNativeDark() { settings.darkNative = !settings.darkNative; saveSettings(); applyNativeFlags(); toast(settings.darkNative ? '已开启夜间模式' : '已关闭夜间模式'); } /* ========================================================================= * 7. 阅读器状态 * ========================================================================= */ const state = { open: false, bookId: null, firstIndex: null, lastIndex: null, total: null, bookTitle: '', loading: false, reachedEnd: false, toc: null, lastScrollSave: 0, // 自动滚动 scrollOn: false, scrollRaf: null, scrollAcc: 0, scrollLastTs: null }; function progressMap() { const p = readStore('trxs:progress', {}); return (p && typeof p === 'object') ? p : {}; } function saveProgress() { if (!state.bookId || !state.firstIndex) return; const p = progressMap(); p[state.bookId] = { index: state.firstIndex, title: state.bookTitle || '', total: state.total || 0, ts: Date.now() }; writeStore('trxs:progress', p); } /* ========================================================================= * 8. 阅读器 DOM * ========================================================================= */ function buildReader() { if ($('#trx-reader')) return; const wrap = document.createElement('div'); wrap.id = 'trx-reader'; wrap.dataset.theme = settings.theme; wrap.innerHTML = `