// ==UserScript==
// @name 阅读模式·纯净版
// @namespace https://viayoo.com/hqzw7k
// @version 21.0.0
// @description 在当前页面之上叠加一层纯净阅读界面,原页面冻结于下方,退出后自动恢复。自动识别正文、标题、翻页链接及目录,过滤视频、样式、脚本、按钮、广告等干扰。支持背景色、字号、夜间模式、动态切章(无刷新)、下一章快速切换、目录抽屉、进度条跳章、新标签页打开。
// @author DeepSeek
// @homepageURL https://scriptcat.org/zh-CN/users/184036
// @license MIT
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
const CONTENT_SELECTORS = [
'#chapter-content', '#chaptercontent', '#ChapterContent', '#nr1', '#nr_title', '#booktext',
'#novel_content', '#read-content', '#chapter-body', '#txtContent', '#BookText',
'#chapter', '#contentBox', '#book_read', '.read-content', '.chapter-content',
'#J_read', '#reader', '.article-content', '.content-area', '.post-body',
'#js-read__content', '#readbox', '#zhengwen', '#kui-page-read-txt', '#chapter_cont',
'#BookTextRead', '#book_text', '#readtext', '#readcon', '#TextContent', '#text_c',
'#txt_td', '#TXT', '#txt', '#zjneirong', '.novel_content', '.readmain_inner',
'.noveltext', '.booktext', '.yd_text2', '#contentTxt', '#oldtext', '#a_content',
'#contents', '#content2', '#contentts', '#content1', '#novelcontent', '#text',
'.m-post', '.novel-content', 'pre.novel-content-txt', '.share-v2-chapter-content',
'.content_ul', '.file_content', '.submenu + .con',
'#article', '#articleBody', '#article-body', '#post-content', '#entry-content',
'#main-content', '#content-main', '#post', '.article-body', '.post-content',
'.entry-content', '.post-body', '.entry-body', '.article-content', '.content-inner',
'.articleText', '#js_content', '#baikan-content', '.news-content', '.rm_txt_con',
'#reader-container', '.rich_media_content', '#detail', '.detail_nr', '#chapterDiv',
'.article_content', '#contentText', '.fallback-content', '#mw-content-text',
'#content', '.article',
'[itemprop="articleBody"]', '[itemprop="text"]', '[role="article"]',
'.readable-content', '.post-article', '.markdown-body', 'article',
'main', '[role="main"]', '.post', '.blog-post', '.story', '.content-wrapper'
];
const TITLE_SELECTORS = [
'h1.article-title', 'h1.entry-title', 'h1.post-title', 'h1.title',
'h1#title', 'h1#articleTitle', 'h1[itemprop="headline"]',
'.nr_function>h1', 'h1', '.article-title', '.entry-title', '.post-title',
'.title', '#title', '#article-title', '#post-title', '.title-color', 'h2.big.o'
];
const NAV_SELECTORS = {
prev: [
'a:contains(上一页)', 'a:contains(上一章)', 'a:contains(上一篇)',
'a.prev', 'a.previous', '.prev', '.previous', '#prev', '#previous',
'a[rel="prev"]', 'a[rel="previous"]', 'a[data-prev]'
],
next: [
'a:contains(下一页)', 'a:contains(下一章)', 'a:contains(下一篇)',
'a.next', '.next', '#next', 'a[rel="next"]', 'a[data-next]'
],
index: [
'a:contains(目录)', 'a:contains(章节目录)', 'a:contains(返回目录)',
'a:contains(书籍页)', 'a:contains(章节列表)',
'a.index', '.index', '#index', 'a.toc', '.toc', '#toc', 'a[rel="index"]'
]
};
const INDEX_URL_HINTS = /indexlist|chapterlist|catalog|mulu|list/i;
const EXTRA_REMOVE_SELECTORS = [
'.aritcle_card', '.projects-header', '.card-list', '.apd-bg',
'#comments', '.comments', '.url-card', '.commits-part',
'.fold_arrow', '.apd'
];
const EXTRA_REMOVE_TEXTS = [
'举报', '回复'
];
const EXTRA_REMOVE_TEXT_PATTERNS = [
'第*章'
];
const INTERACTIVE_SELECTOR = 'a, img, button, summary, details, input, label, select, textarea, video, audio, [onclick], [role="button"], [contenteditable="true"]';
const SHOULD_ACTIVATE_NEW_TAB = window.location.hash === '#readermode';
const ICONS = {
back: ``,
open: ``,
catalog: ``,
settings: ``,
moon: ``,
sun: ``,
crosshair: ``,
prev: ``,
next: ``,
bg: ``,
translate: ``,
catalogSortAsc: ``,
catalogSortDesc: ``,
missing: ``
};
const STORAGE_KEYS = {
BG_COLOR: 'reader_bg_color',
FONT_SIZE: 'reader_font_size',
THEME_MODE: 'reader_theme_mode',
};
const DEFAULT_SETTINGS = {
bgColor: '#ffffff',
fontSize: '18px',
themeMode: 'day',
};
const MIN_CONTENT_LENGTH = 50;
const settingsCache = {};
function gmGet(key, def) {
if (settingsCache[key] !== undefined) return settingsCache[key];
try {
const v = GM_getValue(key);
if (v !== undefined && v !== null && v !== '') {
settingsCache[key] = v;
return v;
}
} catch(e) {}
return def;
}
function gmSet(key, val) {
settingsCache[key] = val;
try { GM_setValue(key, val); } catch(e) {}
}
function loadSettings() {
return {
bgColor: gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor),
fontSize: gmGet(STORAGE_KEYS.FONT_SIZE, DEFAULT_SETTINGS.fontSize),
themeMode: normalizeThemeMode(gmGet(STORAGE_KEYS.THEME_MODE, DEFAULT_SETTINGS.themeMode))
};
}
function normalizeThemeMode(mode) {
return mode === 'night' ? 'night' : 'day';
}
function saveSetting(key, value) { gmSet(key, value); }
let toastHost = null;
let toastShadow = null;
let toastContainer = null;
function ensureToastHost() {
if (toastHost && toastShadow && toastContainer && document.documentElement.contains(toastHost)) return;
toastHost = document.createElement('div');
toastHost.id = 'reader-toast-host';
toastHost.style.cssText = 'all:initial;position:fixed;top:0;left:0;width:0;height:0;z-index:2147483647;pointer-events:none;';
toastShadow = toastHost.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
.toast-container { position:fixed; left:0; right:0; bottom:75px; display:flex; flex-direction:column; align-items:center; gap:8px; pointer-events:none; z-index:2147483647; }
.toast-item { background:rgba(28,28,30,0.85); color:#fff; padding:14px 24px; border-radius:999px; font-size:14px; font-weight:500; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; box-shadow:0 8px 24px rgba(0,0,0,0.2); animation:toastIn 0.3s cubic-bezier(0.22,1,0.36,1); opacity:0; transform:translateY(20px); animation-fill-mode:forwards; max-width:80%; text-align:center; word-break:break-word; }
.toast-item.out { animation:toastOut 0.3s cubic-bezier(0.22,1,0.36,1) forwards; }
@keyframes toastIn { from { opacity:0; transform:translateY(20px); } to { opacity:1; transform:translateY(0); } }
@keyframes toastOut { from { opacity:1; transform:translateY(0); } to { opacity:0; transform:translateY(20px); } }
`;
toastShadow.appendChild(style);
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container';
toastShadow.appendChild(toastContainer);
document.documentElement.appendChild(toastHost);
}
function showShadowToast(msg, duration = 2000) {
try {
ensureToastHost();
const item = document.createElement('div');
item.className = 'toast-item';
item.textContent = msg;
toastContainer.appendChild(item);
setTimeout(() => {
item.classList.add('out');
setTimeout(() => { try { item.remove(); } catch(e) {} }, 320);
}, duration);
} catch(e) {}
}
function notify(msg) {
if (window.via && typeof window.via.toast === 'function') {
try {
window.via.toast(msg);
return;
} catch(e) {}
}
showShadowToast(msg);
}
function normalizeUrl(u) {
try {
const url = new URL(u, window.location.href);
return url.origin + url.pathname;
} catch(e) { return u; }
}
let isReaderMode = false;
let buttonHideTimeout = null;
let mainButton = null;
let originalUrl = '';
let menuCommandIds = [];
let readerOverlay = null;
let newTabOverlay = null;
let originalBodyOverflow = '';
let originalBodyPosition = '';
let originalBodyTop = '';
let originalBodyWidth = '';
let scrollY = 0;
let currentCatalogView = null;
let catalogCache = null;
let catalogCacheLoading = null;
let chapterCache = new Map();
let currentPreloadTask = null;
let preloadScheduleTimer = null;
let catalogOrder = 'asc';
const config = {
backgroundOptions: {
'默认白': '#ffffff', '豆沙绿': '#c7edcc',
'护眼绿': '#e3f2e1', '藕粉': '#f5e6e8',
'护眼黑': '#1a1a1a'
}
};
const NIGHT_THEME = {
bgColor: '#111d2b',
textColor: '#c0c5cc',
borderColor: '#2a3b4d',
controlBgColor: '#172535',
controlBorderColor: '#2a3b4d',
subtleTextColor: '#8892a0',
exitBtnColor: '#8892a0',
navBtnColor: '#4a9eff',
codeBg: 'rgba(255,255,255,0.05)',
codeBorder: 'rgba(255,255,255,0.18)',
codeTextColor: '#e6edf3',
inlineCodeBg: 'rgba(255,255,255,0.12)',
tableHeaderBg: 'rgba(255,255,255,0.06)',
tableBorder: 'rgba(255,255,255,0.18)'
};
function isValidHref(h) {
if (!h || h.trim() === '' || h === '#') return false;
try {
const url = new URL(h, window.location.href);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch (e) { return false; }
}
function removeSimulatedButtons(c) {
c.querySelectorAll('[role="button"]:not(a)').forEach(e => e.remove());
c.querySelectorAll('[rl-type="stop"]:not(a)').forEach(e => e.remove());
const kw = ['btn', 'button', 'copy', 'share', 'feedback', 'thumb', 'like', 'dislike', 'close', 'popup', 'modal', 'toolbar', 'interact', 'action'];
const s = kw.map(k => `[class*="${k}"]:not(a)`).join(',');
c.querySelectorAll(s).forEach(e => e.remove());
}
function stripLeadingSpaces(c) {
const blockTags = ['p', 'div', 'li', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
c.querySelectorAll(blockTags.join(',')).forEach(el => {
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
const first = walker.nextNode();
if (first) {
first.nodeValue = first.nodeValue.replace(/^[\s\u00A0\u3000\u2000-\u200B\uFEFF]+/, '');
}
});
c.querySelectorAll('pre').forEach(pre => {
const first = pre.firstChild;
if (first && first.nodeType === 3 && /^\s*$/.test(first.nodeValue)) first.nodeValue = '';
});
}
function splitBrParagraphs(root) {
const BLOCK_TAGS = new Set([
'DIV', 'P', 'UL', 'OL', 'TABLE', 'BLOCKQUOTE', 'PRE',
'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
'SECTION', 'ARTICLE', 'DL', 'FIGURE', 'FORM',
'HEADER', 'FOOTER', 'NAV', 'MAIN', 'ASIDE', 'DETAILS'
]);
const allDivs = Array.from(root.querySelectorAll('div'));
for (const block of allDivs) {
try {
processBlock(block);
} catch(e) {}
}
function processBlock(block) {
const nodes = Array.from(block.childNodes);
const hasContent = nodes.some(n =>
(n.nodeType === 3 && (n.nodeValue || '').trim().length > 0) ||
(n.nodeType === 1 && n.tagName === 'BR')
);
if (!hasContent) return;
const rawText = (block.textContent || '').trim();
if (rawText.length < 50) return;
const groups = [];
let current = [];
let brCount = 0;
const flushCurrent = () => {
if (current.length > 0) {
groups.push({ type: 'p', nodes: current });
current = [];
}
};
for (const node of nodes) {
if (node.nodeType === 1 && node.tagName === 'BR') {
brCount++;
if (brCount >= 2) {
flushCurrent();
brCount = 0;
} else {
current.push(node);
}
} else if (node.nodeType === 1 && BLOCK_TAGS.has(node.tagName)) {
flushCurrent();
groups.push({ type: 'keep', node });
brCount = 0;
} else if (node.nodeType === 1 || node.nodeType === 3) {
brCount = 0;
current.push(node);
}
}
flushCurrent();
const pGroups = groups.filter(g => g.type === 'p');
if (pGroups.length === 0) return;
const totalPText = pGroups.reduce((s, g) => {
return s + g.nodes.reduce((s2, n) => s2 + (n.textContent || '').replace(/\s+/g, '').length, 0);
}, 0);
if (totalPText < 50) return;
while (block.firstChild) block.removeChild(block.firstChild);
for (const group of groups) {
if (group.type === 'keep') {
block.appendChild(group.node);
} else {
const p = document.createElement('p');
for (const n of group.nodes) p.appendChild(n);
if (p.textContent.trim().length > 0 || p.querySelector('img')) {
block.appendChild(p);
}
}
}
}
}
function wrapLooseTextNodes(root) {
const BLOCK_TAGS = new Set([
'DIV', 'P', 'UL', 'OL', 'TABLE', 'BLOCKQUOTE', 'PRE',
'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
'SECTION', 'ARTICLE', 'DL', 'FIGURE', 'FORM',
'HEADER', 'FOOTER', 'NAV', 'MAIN', 'ASIDE', 'DETAILS',
'HR'
]);
const divs = Array.from(root.querySelectorAll('div'));
for (const div of divs) {
try {
const children = Array.from(div.childNodes);
if (children.some(n => n.nodeType === 1 && n.tagName === 'P')) continue;
const hasBlockChild = children.some(n =>
n.nodeType === 1 && BLOCK_TAGS.has(n.tagName)
);
if (!hasBlockChild) continue;
const hasLooseText = children.some(n =>
n.nodeType === 3 && (n.nodeValue || '').trim().length > 0
);
if (!hasLooseText) continue;
const newChildren = [];
let buffer = [];
const flush = () => {
if (buffer.length === 0) return;
const hasContent = buffer.some(n => {
if (n.nodeType === 3) return (n.nodeValue || '').trim().length > 0;
return true;
});
if (!hasContent) { buffer = []; return; }
const p = document.createElement('p');
for (const n of buffer) {
if (n.nodeType === 3) {
const parts = (n.nodeValue || '').split('\n');
for (let i = 0; i < parts.length; i++) {
if (i > 0) p.appendChild(document.createElement('br'));
if (parts[i]) p.appendChild(document.createTextNode(parts[i]));
}
} else {
p.appendChild(n);
}
}
newChildren.push(p);
buffer = [];
};
for (const node of children) {
if (node.nodeType === 1 && BLOCK_TAGS.has(node.tagName)) {
flush();
newChildren.push(node);
} else {
buffer.push(node);
}
}
flush();
while (div.firstChild) div.removeChild(div.firstChild);
for (const n of newChildren) div.appendChild(n);
} catch(e) {}
}
}
function buildTextPatternRegex(pattern) {
const parts = pattern.split('*').map(p => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const body = parts.join('[\\d一二三四五六七八九十百千万零两]+');
return new RegExp('^' + body + '(\\s|$|[::.、\\-—])');
}
function cleanContent(c, shouldSplit) {
c.querySelectorAll('[style*="display: none"], [style*="display:none"], [style*="visibility: hidden"], [style*="visibility:hidden"]').forEach(e => e.remove());
c.querySelectorAll('markdown-accessiblity-table').forEach(el => {
const p = el.parentNode;
if (p) { while (el.firstChild) p.insertBefore(el.firstChild, el); p.removeChild(el); }
});
for (const s of EXTRA_REMOVE_SELECTORS) c.querySelectorAll(s).forEach(e => e.remove());
if (EXTRA_REMOVE_TEXTS.length) {
const esc = t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const exact = new Set(EXTRA_REMOVE_TEXTS);
const suffixRe = new RegExp('^(' + EXTRA_REMOVE_TEXTS.map(esc).join('|') + ')[((]\\d*[))]?$');
c.querySelectorAll('a').forEach(a => {
const t = (a.textContent || '').trim();
if (exact.has(t) || suffixRe.test(t)) a.remove();
});
}
if (EXTRA_REMOVE_TEXT_PATTERNS.length) {
const patternRes = EXTRA_REMOVE_TEXT_PATTERNS.map(buildTextPatternRegex);
c.querySelectorAll('h1, h2, h3, h4, h5, h6, p').forEach(el => {
const t = (el.textContent || '').trim();
if (t && patternRes.some(re => re.test(t))) el.remove();
});
}
c.querySelectorAll('video, style, link[rel="stylesheet"], script, button, svg').forEach(e => e.remove());
c.querySelectorAll('form, textarea, select, input').forEach(e => e.remove());
removeSimulatedButtons(c);
c.querySelectorAll('i').forEach(e => {
if (!e.textContent.trim() && !e.querySelector('img')) e.remove();
});
c.querySelectorAll('figure.highlight, div.highlight, figure[class*="highlight"], div[class*="highlight"]').forEach(container => {
const pre = container.querySelector('pre');
if (!pre) return;
const table = container.querySelector('table');
if (!table) return;
const tableText = (table.textContent || '').replace(/\s+/g, '');
const preText = (pre.textContent || '').replace(/\s+/g, '');
if (tableText !== preText) return;
const preClone = pre.cloneNode(true);
while (container.firstChild) container.removeChild(container.firstChild);
container.appendChild(preClone);
});
c.querySelectorAll('*').forEach(e => { e.removeAttribute('style'); e.removeAttribute('class'); });
['mark', 'span', 'ml-search'].forEach(t => {
c.querySelectorAll(t).forEach(e => {
const p = e.parentNode;
if (p) { while (e.firstChild) p.insertBefore(e.firstChild, e); p.removeChild(e); }
});
});
c.querySelectorAll('a').forEach(a => {
const h = a.getAttribute('href');
if (!h || !isValidHref(h)) {
const p = a.parentNode;
if (p) { while (a.firstChild) p.insertBefore(a.firstChild, a); p.removeChild(a); }
return;
}
const attrs = a.attributes;
for (let i = attrs.length - 1; i >= 0; i--) {
if (attrs[i].name !== 'href') a.removeAttribute(attrs[i].name);
}
});
c.querySelectorAll('img').forEach(img => {
const src = img.getAttribute('src')
|| img.getAttribute('data-original')
|| img.getAttribute('data-src')
|| img.getAttribute('data-lazy-src')
|| img.getAttribute('data-echo')
|| '';
const rawClass = img.getAttribute('class') || '';
const isEmoji = /emotion|emoji/i.test(src) || /emoji/i.test(rawClass);
const altVal = img.getAttribute('alt') || '';
const widthVal = img.getAttribute('width') || '';
const heightVal = img.getAttribute('height') || '';
const attrs = img.attributes;
for (let i = attrs.length - 1; i >= 0; i--) {
img.removeAttribute(attrs[i].name);
}
if (altVal) img.setAttribute('alt', altVal);
if (src) {
img.setAttribute('src', src);
if (isEmoji) {
img.setAttribute('data-emoji', '1');
} else {
if (widthVal) img.setAttribute('width', widthVal);
if (heightVal) img.setAttribute('height', heightVal);
}
} else {
img.remove();
}
});
if (shouldSplit) splitBrParagraphs(c);
wrapLooseTextNodes(c);
stripLeadingSpaces(c);
}
function detectIndent(el) {
const blocks = el.querySelectorAll('p, li, div');
if (blocks.length > 0) {
let cnt = 0;
const n = Math.min(blocks.length, 10);
for (let i = 0; i < n; i++) {
const ti = parseFloat(window.getComputedStyle(blocks[i]).textIndent) || 0;
if (ti > 1) cnt++;
}
if (cnt > n / 2) return true;
}
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
let textCount = 0, indentCount = 0, node;
while ((node = walker.nextNode()) !== null && textCount < 30) {
const text = node.nodeValue || '';
if (text.trim().length < 5) continue;
textCount++;
if (/^(?:\u3000\u3000|\u3000|\s{3,})/.test(text)) indentCount++;
}
return textCount > 0 && indentCount > textCount * 0.5;
}
function findLinkByText(doc, selectors) {
for (const s of selectors) {
if (s.includes(':contains(')) {
const m = s.match(/:contains\((.+?)\)/);
if (m) {
const text = m[1];
const base = s.replace(/:contains\(.+?\)/, '');
const els = base ? doc.querySelectorAll(base) : doc.querySelectorAll('a');
for (const el of els) {
if (el.tagName === 'A' && el.href && el.textContent.includes(text) && isValidHref(el.href)) return el;
}
}
} else {
const el = doc.querySelector(s);
if (el && el.tagName === 'A' && el.href && isValidHref(el.href)) return el;
}
}
return null;
}
function collectLinksByText(doc, selectors) {
const results = [];
const seen = new Set();
for (const s of selectors) {
if (s.includes(':contains(')) {
const m = s.match(/:contains\((.+?)\)/);
if (m) {
const text = m[1];
const base = s.replace(/:contains\(.+?\)/, '');
let els;
try { els = base ? doc.querySelectorAll(base) : doc.querySelectorAll('a'); }
catch(e) { continue; }
for (const el of els) {
if (el.tagName === 'A' && el.href && el.textContent.includes(text) && isValidHref(el.href)) {
const key = el.href;
if (!seen.has(key)) { seen.add(key); results.push(el); }
}
}
}
} else {
let els;
try { els = doc.querySelectorAll(s); }
catch(e) { continue; }
for (const el of els) {
if (el.tagName === 'A' && el.href && isValidHref(el.href)) {
const key = el.href;
if (!seen.has(key)) { seen.add(key); results.push(el); }
}
}
}
}
return results;
}
function findNavLinksByContext(doc) {
const res = { prev: null, next: null, index: null };
const xpaths = {
prev: '//*[contains(text(),"上一章") or contains(text(),"上一页") or contains(text(),"上一篇")]/following-sibling::a[1]',
next: '//*[contains(text(),"下一章") or contains(text(),"下一页") or contains(text(),"下一篇")]/following-sibling::a[1]',
index: '//*[contains(text(),"目录") or contains(text(),"章节目录") or contains(text(),"返回目录") or contains(text(),"书籍页")]/following-sibling::a[1]'
};
for (let key in xpaths) {
const node = doc.evaluate(xpaths[key], doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (node && node.tagName === 'A' && node.href && isValidHref(node.href)) res[key] = node;
}
const containers = [
{ key: 'prev', texts: ['上一章', '上一页', '上一篇'] },
{ key: 'next', texts: ['下一章', '下一页', '下一篇'] },
{ key: 'index', texts: ['目录', '章节目录', '返回目录', '书籍页'] }
];
for (const item of containers) {
if (!res[item.key]) {
for (const txt of item.texts) {
const container = doc.evaluate(`//*[contains(text(),"${txt}")]`, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (container) {
const link = container.querySelector('a[href]');
if (link && isValidHref(link.href)) { res[item.key] = link; break; }
}
}
}
}
return res;
}
function findNavLinks(doc) {
doc = doc || document;
let links = {
prev: findLinkByText(doc, NAV_SELECTORS.prev),
next: findLinkByText(doc, NAV_SELECTORS.next),
index: findLinkByText(doc, NAV_SELECTORS.index)
};
const cl = findNavLinksByContext(doc);
if (!links.prev && cl.prev) links.prev = cl.prev;
if (!links.next && cl.next) links.next = cl.next;
if (!links.index && cl.index) links.index = cl.index;
try {
const indexCandidates = collectLinksByText(doc, NAV_SELECTORS.index);
if (cl.index && !indexCandidates.some(a => a.href === cl.index.href)) {
indexCandidates.push(cl.index);
}
if (indexCandidates.length > 0) {
indexCandidates.sort((a, b) => {
const aHit = INDEX_URL_HINTS.test(a.href) ? 0 : 1;
const bHit = INDEX_URL_HINTS.test(b.href) ? 0 : 1;
return aHit - bHit;
});
const best = indexCandidates[0];
if (best && best.href && isValidHref(best.href)) {
if (!links.index || links.index.href !== best.href) {
// console.log(`[阅读模式] 目录优选:主目录 ${links.index ? links.index.href : '(无)'} → ${best.href}`);
}
links.index = best;
}
}
links.indexCandidates = indexCandidates.map(a => a.href);
} catch(e) {}
return links;
}
function extractContent(doc) {
doc = doc || document;
let contentElement = null;
for (const s of CONTENT_SELECTORS) {
const els = doc.querySelectorAll(s);
if (els.length) {
let max = els[0];
let maxLen = els[0].innerHTML.length;
for (let i = 1; i < els.length; i++) {
const len = els[i].innerHTML.length;
if (len > maxLen) { maxLen = len; max = els[i]; }
}
contentElement = max;
// console.log(`阅读模式: 使用选择器 "${s}"`);
break;
}
}
if (!contentElement) return null;
const autoHasIndent = detectIndent(contentElement);
const clone = contentElement.cloneNode(true);
cleanContent(clone, autoHasIndent);
const html = clone.innerHTML.trim();
const textContent = clone.textContent.trim();
if (!html || !textContent || textContent.length <= MIN_CONTENT_LENGTH) return null;
let title = '';
for (const s of TITLE_SELECTORS) {
const el = doc.querySelector(s);
if (el && el.textContent.trim()) {
if (el.querySelector('a')) {
const cloneEl = el.cloneNode(true);
cloneEl.querySelectorAll('a').forEach(a => a.remove());
title = cloneEl.textContent
.replace(/^[\s>»›\-—::]+/, '')
.trim();
} else {
title = el.textContent.trim();
}
// console.log(`阅读模式: 使用标题选择器 "${s}" 匹配到标题: "${title}"`);
break;
}
}
if (!title) {
title = doc.title || '';
// console.log(`阅读模式: 使用网页标题: "${title}"`);
}
return { title, content: html, nav: findNavLinks(doc), autoHasIndent };
}
function addReaderModeHash(url) {
try {
const u = new URL(url, window.location.origin);
u.hash = 'readermode';
u.searchParams.set('reader_trigger', 'menu');
return u.href;
} catch(e) {
let result = url;
if (!result.includes('#')) result += '#readermode';
result += result.includes('?') ? '&reader_trigger=menu' : '?reader_trigger=menu';
return result;
}
}
function isDarkColor(bg) {
if (!bg) return false;
let r, g, b;
if (bg.startsWith('#') && (bg.length === 7 || bg.length === 4)) {
let hex = bg.slice(1);
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
r = parseInt(hex.slice(0, 2), 16);
g = parseInt(hex.slice(2, 4), 16);
b = parseInt(hex.slice(4, 6), 16);
} else if (bg.startsWith('rgb')) {
const m = bg.match(/\d+/g);
if (!m || m.length < 3) return false;
r = parseInt(m[0], 10); g = parseInt(m[1], 10); b = parseInt(m[2], 10);
} else {
return false;
}
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance < 0.5;
}
function calculateColors(bg, isNight) {
if (isNight) {
return {
textColor: NIGHT_THEME.textColor,
borderColor: NIGHT_THEME.borderColor,
controlBgColor: NIGHT_THEME.controlBgColor,
controlBorderColor: NIGHT_THEME.controlBorderColor,
subtleTextColor: NIGHT_THEME.subtleTextColor,
exitBtnColor: NIGHT_THEME.exitBtnColor,
navBtnColor: NIGHT_THEME.navBtnColor,
codeBg: NIGHT_THEME.codeBg,
codeBorder: NIGHT_THEME.codeBorder,
codeTextColor: NIGHT_THEME.codeTextColor,
inlineCodeBg: NIGHT_THEME.inlineCodeBg,
tableHeaderBg: NIGHT_THEME.tableHeaderBg,
tableBorder: NIGHT_THEME.tableBorder
};
}
const dark = isDarkColor(bg);
return {
textColor: dark ? '#e0e0e0' : '#000',
borderColor: dark ? '#444' : '#eee',
controlBgColor: bg,
controlBorderColor: dark ? '#555' : '#ddd',
subtleTextColor: dark ? '#aaa' : '#666',
exitBtnColor: dark ? '#999' : '#666',
navBtnColor: dark ? '#4a9eff' : '#007bff',
codeBg: dark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)',
codeBorder: dark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)',
codeTextColor: dark ? '#e6edf3' : '#24292e',
inlineCodeBg: dark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.06)',
tableHeaderBg: dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)',
tableBorder: dark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)'
};
}
function getEffectiveBgColor(userBg, isNight) {
return isNight ? NIGHT_THEME.bgColor : userBg;
}
function getAccentColor(userBg, isNight) {
if (isNight) return NIGHT_THEME.navBtnColor;
return isDarkColor(userBg) ? '#4a9eff' : '#007bff';
}
function getIndentRule(autoHasIndent) {
return autoHasIndent
? '#reader-content-section p, #reader-content-section li{text-indent:2em;}'
: '#reader-content-section p, #reader-content-section li{text-indent:0;}';
}
function buildContentStyleCSS(userBg, autoHasIndent, isNight) {
const colors = calculateColors(userBg, isNight);
const indentRule = getIndentRule(autoHasIndent);
return `
#reader-content-section p,
#reader-content-section div,
#reader-content-section h1, #reader-content-section h2, #reader-content-section h3,
#reader-content-section h4, #reader-content-section h5, #reader-content-section h6,
#reader-content-section li,
#reader-content-section blockquote,
#reader-content-section pre,
#reader-content-section table,
#reader-content-section td,
#reader-content-section th {
max-width: 100%;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
box-sizing: border-box;
}
#reader-content-section p {
text-align: justify;
margin: 0 0 1.8em 0;
white-space: normal !important;
}
${indentRule}
#reader-content-section img { max-width: 100%; height: auto; }
#reader-content-section img[data-emoji] {
display: inline; max-width: none;
width: 1.2em; height: 1.2em; vertical-align: -0.15em;
}
#reader-content-section p > img:only-child,
#reader-content-section div > img:only-child {
display: block;
margin: 1.2em auto;
}
#reader-content-section blockquote {
margin-left: 0; padding-left: 1.2em;
border-left: 3px solid ${colors.borderColor};
color: ${colors.subtleTextColor};
}
#reader-content-section a { color: ${colors.navBtnColor}; text-decoration: none; cursor: pointer; }
#reader-content-section a:hover { text-decoration: none; }
#reader-content-section strong, #reader-content-section b { font-weight: bold; }
#reader-content-section em, #reader-content-section i { font-style: italic; }
#reader-content-section h1, #reader-content-section h2, #reader-content-section h3,
#reader-content-section h4, #reader-content-section h5, #reader-content-section h6 {
font-weight: bold; margin-top: 1.2em; margin-bottom: 0.6em;
}
#reader-content-section h1 { font-size: 1.8em; }
#reader-content-section h2 { font-size: 1.5em; margin-top: 2em; margin-bottom: 0.8em; }
#reader-content-section h3 { font-size: 1.3em; }
#reader-content-section ul, #reader-content-section ol {
padding-left: 0; margin: 0.5em 0; list-style: none;
}
#reader-content-section li {
display: block; list-style: none; margin: 0.2em 0;
padding-left: 1.2em; position: relative;
}
#reader-content-section ul > li::before {
content: ''; position: absolute; left: 0.2em; top: 0.85em;
width: 5px; height: 5px; border-radius: 50%;
background: currentColor;
}
#reader-content-section code {
background: ${colors.inlineCodeBg}; border-radius: 3px;
padding: 0.15em 0.4em;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 0.92em; color: ${colors.codeTextColor};
}
#reader-content-section pre {
background: ${colors.codeBg}; border: 1px solid ${colors.codeBorder};
border-radius: 6px; padding: 12px 16px; overflow-x: auto;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 0.92em; line-height: 1.6; white-space: pre;
margin: 0 0 1.8em 0;
}
#reader-content-section pre code {
background: none; border: none; padding: 0; border-radius: 0;
font-size: inherit; color: inherit;
}
#reader-content-section table {
border-collapse: collapse; width: 100%; margin: 1em 0;
}
#reader-content-section th,
#reader-content-section td {
border: 1px solid ${colors.tableBorder}; padding: 6px 10px; text-align: left;
}
#reader-content-section th { background: ${colors.tableHeaderBg}; font-weight: 600; }
`;
}
function chineseNumToArabic(s) {
if (!s) return null;
if (/^\d+$/.test(s)) return parseInt(s, 10);
const digitMap = { '零': 0, '一': 1, '二': 2, '两': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9 };
const unitMap = { '十': 10, '百': 100, '千': 1000 };
let total = 0;
let cur = 0;
for (const ch of s) {
if (digitMap[ch] !== undefined) {
cur = cur * 10 + digitMap[ch];
} else if (unitMap[ch] !== undefined) {
const u = unitMap[ch];
if (cur === 0) cur = 1;
total += cur * u;
cur = 0;
}
}
total += cur;
return total > 0 ? total : null;
}
function extractNumFromTitle(text) {
if (!text) return null;
const m = text.match(/第\s*([0-90-9]+|[一二三四五六七八九十百千万零两]+)\s*[章节回卷话集]/);
if (!m) return null;
return chineseNumToArabic(m[1]);
}
function sortChapterLinksByOrder(links, extractNumFromHref) {
if (!links || links.length < 2) return links;
let titleHit = 0, total = 0;
const titleNums = new Map();
for (const l of links) {
if (l.__missing) continue;
total++;
const n = extractNumFromTitle(l.text);
if (n !== null) { titleNums.set(l, n); titleHit++; }
}
const titleReliable = total > 0 && titleHit >= total * 0.8;
const entries = [];
for (let i = 0; i < links.length; i++) {
const l = links[i];
if (l.__missing) {
if (l._num !== null && l._num !== undefined) {
entries.push({ pos: i, key: l._num, item: l });
}
continue;
}
if (titleReliable) {
const tn = titleNums.get(l);
if (tn !== null && tn !== undefined) {
entries.push({ pos: i, key: tn, item: l });
}
} else {
const un = extractNumFromHref(l.href);
if (un !== null && un !== undefined) {
entries.push({ pos: i, key: un, item: l });
}
}
}
if (entries.length < 2) return links;
const sorted = entries.slice().sort((a, b) => a.key - b.key);
let changed = 0;
for (let i = 0; i < entries.length; i++) {
if (links[entries[i].pos] !== sorted[i].item) changed++;
links[entries[i].pos] = sorted[i].item;
}
if (changed > 0) {
// console.log(`[阅读模式] 目录排序:${entries.length} 项带编号章节已按编号排列(无编号章节原位不动)`);
}
return links;
}
function extractMissingChapterItems(doc, indexUrl, existingNums, minNum, maxNum) {
if (!doc) return [];
const results = [];
const seenNums = new Set();
const titleRe = /第\s*(?:[0-90-9]+|[一二三四五六七八九十百千万零两]+)\s*[章节回卷话集]/;
doc.querySelectorAll('li, dd').forEach(el => {
if (el.querySelector('a[href]')) return;
const text = (el.textContent || '').trim();
if (!text || text.length < 2 || text.length > 80) return;
if (!titleRe.test(text)) return;
const num = extractNumFromTitle(text);
if (num === null) return;
if (num < minNum || num > maxNum) return;
if (existingNums.has(num)) return;
if (seenNums.has(num)) return;
seenNums.add(num);
results.push({
href: null,
text,
__missing: true,
_num: num
});
});
return results;
}
function extractChapterLinksFromDoc(doc, indexUrl) {
if (!doc) return [];
const containerSelectors = [
'ul.chapter', '#chapterlist', '.chapterlist', '#list', '.listmain',
'.chapter', '#chapter', '.book_list', '.booklist', '.volume',
'#chapter-list', '.chapter-list', '.mulu', '#mulu', '.catalog',
'.catalog-list', '#readerlist', '.list-chapter', '#list-chapter',
'dl#list', 'dl.chapterlist', '.section-list', '.chapters',
'#jsList1', '.list_li', '.lb_mulu', '.chapterList'
];
function findBestContainer(root) {
let container = null;
let maxLinks = 0;
for (const sel of containerSelectors) {
try {
const els = root.querySelectorAll(sel);
for (const el of els) {
const count = el.querySelectorAll('a').length;
if (count > maxLinks) { maxLinks = count; container = el; }
}
} catch(e) {}
}
return { container, maxLinks };
}
let basePath = '';
let indexOrigin = '';
try {
const u = new URL(indexUrl);
indexOrigin = u.origin;
basePath = u.pathname.replace(/index(_\d+)?\.html?$/i, '');
if (!basePath.endsWith('/')) {
basePath = basePath.replace(/[^/]+$/, '');
}
} catch(e) {}
const excludeTexts = [
'上一页', '下一页', '上一章', '下一章', '首页', '返回', '目录', '书架', '顶部', '底部',
'开始阅读', '加入书架', '存书签', '我的书架', '阅读记录', '回到顶部', '章节目录',
'返回目录', '返回书页', '直达底部', '点击阅读', '换手', '关灯', '倒序', '字体', '大', '中', '小',
'正序', '升序', '降序', '顺序', '逆序', '排序', '展开', '收起',
'展开全部', '收起全部', '全部章节', '完整目录', '查看全部', '显示全部'
];
const DOWNLOAD_TEXT_RE = /下载|TXT|电子书|全本|手机阅读|epub|mobi/i;
const DOWNLOAD_LAST_SEG_RE = /^(?:txt|down|download|dload|xiazai|epub|mobi|pdf|zip|rar)(?:\.html?)?$/i;
function isChapterCandidate(a) {
const href = a.getAttribute('href');
if (!href || href === '#' || href.startsWith('javascript:')) return false;
const text = (a.textContent || '').trim();
if (!text || text.length > 80 || text.length < 2) return false;
const normText = text.replace(/^[\[【((]+/, '').replace(/[\]】))]+$/, '').trim();
if (excludeTexts.some(w => text === w || normText === w)) return false;
if (/^\d{1,4}$/.test(text)) return false;
if (DOWNLOAD_TEXT_RE.test(text)) return false;
try {
const absObj = new URL(href, indexUrl);
const lastSeg = absObj.pathname.split('/').pop() || '';
if (DOWNLOAD_LAST_SEG_RE.test(lastSeg)) return false;
} catch(e) {}
return true;
}
function toAbs(a) {
const href = a.getAttribute('href');
try { return new URL(href, indexUrl).href; } catch(e) { return null; }
}
function extractNumericKey(u) {
try {
const urlObj = new URL(u);
const m = urlObj.pathname.match(/(\d+)(?=\.html?$)/i);
if (m) return parseInt(m[1], 10);
const m2 = urlObj.pathname.match(/(\d+)/g);
if (m2 && m2.length) return parseInt(m2[m2.length - 1], 10);
} catch(e) {}
return null;
}
function scoreOfText(t) {
if (!t) return 0;
let s = 0;
if (/第\s*[\d一二三四五六七八九十百千万零两]+\s*[章节回卷话集]/.test(t)) s += 100;
if (/chapter\s*\d+/i.test(t)) s += 100;
if (/\d/.test(t)) s += 10;
s += Math.min(t.length, 60);
return s;
}
function pathSig(href) {
try {
const u = new URL(href);
const p = u.pathname.replace(/\d+/g, '#').replace(/\/+/g, '/');
return p + (u.search ? '?' + u.search.replace(/\d+/g, '#') : '');
} catch(e) { return href; }
}
function clusterByPathPattern(links) {
if (!links || links.length < 5) return links;
const groups = new Map();
for (const l of links) {
if (!l || l.__missing) continue;
const s = pathSig(l.href);
let arr = groups.get(s);
if (!arr) { arr = []; groups.set(s, arr); }
arr.push(l);
}
if (groups.size <= 1) return links;
let bestPattern = null;
let bestSize = 0;
for (const [s, arr] of groups) {
if (arr.length > bestSize) { bestSize = arr.length; bestPattern = s; }
}
if (bestSize >= 5 && bestSize >= links.length * 0.6) {
const kept = links.filter(l => l.__missing || pathSig(l.href) === bestPattern);
if (kept.length < links.length) {
// console.log(`[阅读模式] 路径模式聚类:${links.length} → ${kept.length} 条`);
}
return kept;
}
return links;
}
function finalizeResult(result) {
if (!result.length) return result;
result = clusterByPathPattern(result);
const existingNums = new Set();
for (const r of result) {
const n = extractNumericKey(r.href);
if (n !== null) existingNums.add(n);
}
if (existingNums.size >= 5) {
const numsArr = Array.from(existingNums).sort((a, b) => a - b);
const minN = numsArr[0];
const maxN = numsArr[numsArr.length - 1];
try {
const missing = extractMissingChapterItems(doc, indexUrl, existingNums, minN, maxN);
if (missing.length > 0) {
for (const m of missing) result.push(m);
}
} catch(e) {}
}
sortChapterLinksByOrder(result, extractNumericKey);
return result;
}
if (basePath && indexOrigin) {
const normBase = basePath.endsWith('/') ? basePath : basePath + '/';
const { container: preContainer, maxLinks: preMax } = findBestContainer(doc);
const scanRoot = (preContainer && preMax >= 5) ? preContainer : doc;
if (scanRoot !== doc) {
// console.log(`[阅读模式] 路径前缀策略:在容器内扫描(${preMax} 条 a)`);
}
const allLinks = Array.from(scanRoot.querySelectorAll('a[href]'));
const seen = new Map();
const result = [];
for (const a of allLinks) {
if (!isChapterCandidate(a)) continue;
const abs = toAbs(a);
if (!abs) continue;
let absObj;
try { absObj = new URL(abs); } catch(e) { continue; }
if (absObj.origin !== indexOrigin) continue;
if (!absObj.pathname.startsWith(normBase)) continue;
if (/index(_\d+)?\.html?$/i.test(absObj.pathname)) continue;
if (absObj.pathname === normBase || absObj.pathname === basePath) continue;
const text = (a.textContent || '').trim();
if (seen.has(abs)) {
const idx = seen.get(abs);
if (scoreOfText(text) > scoreOfText(result[idx].text)) {
result[idx].text = text;
}
continue;
}
seen.set(abs, result.length);
result.push({ href: abs, text });
}
if (result.length >= 5) {
// console.log(`[阅读模式] 目录:基于路径前缀抓取到 ${result.length} 条章节链接`);
sortChapterLinksByOrder(result, extractNumericKey);
return finalizeResult(result);
}
}
let { container, maxLinks } = findBestContainer(doc);
if (!container || maxLinks < 5) {
const candidates = Array.from(doc.querySelectorAll('ul, ol, div, article, section, dl, table'));
let best = null, bestScore = 0;
for (const c of candidates) {
const links = c.querySelectorAll('a[href]');
if (links.length < 5) continue;
let score = 0;
for (const a of links) {
const href = a.getAttribute('href') || '';
if (basePath && href.startsWith(basePath)) score += 3;
else score += 1;
}
if (score > bestScore) { bestScore = score; best = c; }
}
if (best) container = best;
}
const rawLinks = container
? Array.from(container.querySelectorAll('a'))
: Array.from(doc.querySelectorAll('a'));
const seen = new Map();
const result = [];
for (const a of rawLinks) {
if (!isChapterCandidate(a)) continue;
const abs = toAbs(a);
if (!abs) continue;
const text = (a.textContent || '').trim();
if (seen.has(abs)) {
const idx = seen.get(abs);
if (scoreOfText(text) > scoreOfText(result[idx].text)) {
result[idx].text = text;
}
continue;
}
seen.set(abs, result.length);
result.push({ href: abs, text });
}
if (basePath && result.length > 0) {
const sameDir = result.filter(item => {
try { return new URL(item.href).pathname.startsWith(basePath); } catch(e) { return false; }
});
if (sameDir.length >= 5 && sameDir.length >= result.length * 0.5) {
// console.log(`[阅读模式] 目录同路径过滤:${sameDir.length}/${result.length} 条`);
return finalizeResult(sameDir);
}
}
return finalizeResult(result);
}
function safeGetIframeDoc(iframe) {
try {
const doc = iframe.contentDocument;
if (doc) return { doc, error: null };
if (iframe.contentWindow) {
return { doc: iframe.contentWindow.document, error: null };
}
return { doc: null, error: 'empty' };
} catch (e) {
if (e.name === 'SecurityError') {
return { doc: null, error: 'cross-origin' };
}
return { doc: null, error: 'unknown' };
}
}
function preloadCatalog(indexUrl, overlay) {
if (catalogCache && catalogCache.url === indexUrl) return;
if (catalogCacheLoading && catalogCacheLoading.url === indexUrl) return;
catalogCacheLoading = { url: indexUrl };
let finalized = false;
const finalize = (links) => {
if (finalized) return;
finalized = true;
catalogCache = { url: indexUrl, links: links || [] };
catalogCacheLoading = null;
if (links && links.length > 0) {
// console.log(`[阅读模式] 目录预加载成功,共 ${links.length} 条章节链接`);
} else {
// console.error('[阅读模式] 目录预加载失败: 未抓取到章节链接');
}
if (overlay) overlay.dispatchEvent(new CustomEvent('reader-catalog-ready'));
};
const hardTimeout = setTimeout(() => finalize([]), 60000);
function extractNum(u) {
try {
const p = new URL(u).pathname;
const m = p.match(/\/(\d+)\.html?$/i) || p.match(/(\d+)(?=\.html?$)/i);
return m ? parseInt(m[1], 10) : null;
} catch(e) { return null; }
}
function loadDocTemp(url, timeoutMs) {
return new Promise((resolve) => {
const iframe = document.createElement('iframe');
iframe.style.cssText = 'position:absolute;width:0;height:0;border:none;visibility:hidden;';
iframe.src = url;
let done = false;
const cleanup = (result) => {
if (done) return;
done = true;
try { if (iframe.parentNode) iframe.remove(); } catch(e) {}
resolve(result);
};
const t = setTimeout(() => cleanup(null), timeoutMs || 12000);
iframe.onload = () => {
clearTimeout(t);
const result = safeGetIframeDoc(iframe);
cleanup(result.doc);
};
document.body.appendChild(iframe);
});
}
function findPagerUrls(doc, baseUrl) {
if (!doc) return [];
const found = new Set();
const baseKey = normalizeUrl(baseUrl);
const pathBase = (() => {
try {
const u = new URL(baseUrl);
const p = u.pathname;
if (p.endsWith('/')) return p;
return p.replace(/[^/]+$/, '');
} catch(e) { return '/'; }
})();
const tryRoot = (root) => {
if (!root || !root.querySelectorAll) return;
root.querySelectorAll('a[href]').forEach(a => {
const href = a.getAttribute('href');
if (!href || href === '#' || href.startsWith('javascript:')) return;
if (!isValidHref(href)) return;
try {
const absUrl = new URL(href, baseUrl);
const abs = absUrl.href;
if (normalizeUrl(abs) === baseKey) return;
if (!absUrl.pathname.startsWith(pathBase)) return;
const path = absUrl.pathname;
const search = absUrl.search;
if (/\/index_\d+\.html?$/i.test(path) ||
/\/page[-_]?\d+\.html?$/i.test(path) ||
/\/list[-_]?\d+\.html?$/i.test(path) ||
/[?&]page=\d+/i.test(search)) {
found.add(abs);
}
} catch(e) {}
});
};
const containers = [
'.pages', '.pagination', '.page-list', '.pagelist', '.pager',
'.page-nav', '.page_nav', 'nav.pagination', '.listpage',
'.paginate', '.wp-pagenavi', '#pages', '#pagination', '#pagenavi',
'#pagelink', '.pagelink'
];
let hitInContainer = 0;
for (const sel of containers) {
try {
doc.querySelectorAll(sel).forEach(el => {
const before = found.size;
tryRoot(el);
if (found.size > before) hitInContainer++;
});
} catch(e) {}
}
if (hitInContainer === 0 && doc.body) {
tryRoot(doc.body);
}
return Array.from(found);
}
function detectExpandNeed(doc) {
const partialContainers = [
'.catalog-all', '.all-chapter', '.show-all', '.show-more', '.more-chapter',
'.cfirst', '.latest', '.recent', '.newest', '.chapter-more',
'[class*="cfirst"]', '[class*="latest"]', '[class*="recent"]',
'[class*="newest"]', '[class*="all-chapter"]', '[class*="show-all"]'
];
let hasPartialContainer = false;
for (const sel of partialContainers) {
try {
if (doc.querySelector(sel)) { hasPartialContainer = true; break; }
} catch(e) {}
}
const triggerRe = /查看完整|完整目录|完整章节|全部章节|全部目录|展开目录|展开全部|展开更多|显示全部|显示更多|显示完整|加载更多|更多章节|更多目录|查看全部|点击展开|查看所有|所有章节|全部章節|展開目錄|展開全部|加載更多|更多章節|顯示全部/;
const triggerEls = [];
const seen = new Set();
doc.querySelectorAll('a, button, div, span, li, input[type="button"], input[type="submit"]').forEach(el => {
if (seen.has(el)) return;
const t = (el.textContent || '').trim();
if (t && t.length <= 30 && triggerRe.test(t)) {
triggerEls.push(el);
seen.add(el);
}
});
for (const sel of partialContainers) {
try {
doc.querySelectorAll(sel).forEach(el => {
if (!seen.has(el)) { triggerEls.push(el); seen.add(el); }
});
} catch(e) {}
}
return { needsExpand: hasPartialContainer || triggerEls.length > 0, triggerEls };
}
function dispatchRealClick(el) {
if (!el || !el.dispatchEvent) return;
const doc = el.ownerDocument;
const win = doc && doc.defaultView;
const base = { bubbles: true, cancelable: true, view: win || undefined };
try { el.dispatchEvent(new PointerEvent('pointerdown', base)); } catch(e) {}
try { el.dispatchEvent(new MouseEvent('mousedown', base)); } catch(e) {}
try { el.dispatchEvent(new PointerEvent('pointerup', base)); } catch(e) {}
try { el.dispatchEvent(new MouseEvent('mouseup', base)); } catch(e) {}
try { el.click(); } catch(e) {}
}
async function runPagerStrategy(pagerUrls, initialLinks) {
const allUrls = [indexUrl];
const seenUrls = new Set([normalizeUrl(indexUrl)]);
for (const u of pagerUrls) {
const k = normalizeUrl(u);
if (!seenUrls.has(k)) {
seenUrls.add(k);
allUrls.push(u);
}
}
const collectedLinks = [];
const seenHrefs = new Map();
const pushLinks = (links) => {
for (const l of links) {
const k = l.__missing ? ('__missing__' + l._num) : normalizeUrl(l.href);
if (!seenHrefs.has(k)) {
seenHrefs.set(k, collectedLinks.length);
collectedLinks.push(l);
}
}
};
pushLinks(initialLinks);
const MAX_CONCURRENT = 3;
let nextIdx = 1;
const workers = [];
for (let w = 0; w < MAX_CONCURRENT; w++) {
workers.push((async () => {
while (!finalized) {
const myIdx = nextIdx++;
if (myIdx >= allUrls.length) return;
const url = allUrls[myIdx];
const doc = await loadDocTemp(url, 12000);
if (finalized) return;
if (doc) {
try {
const links = extractChapterLinksFromDoc(doc, url);
pushLinks(links);
} catch(e) {}
}
}
})());
}
await Promise.all(workers);
if (finalized) return;
sortChapterLinksByOrder(collectedLinks, extractNum);
clearTimeout(hardTimeout);
finalize(collectedLinks);
}
function runExpandStrategy(doc, triggerEls, iframe, initialLinks) {
const fireClicks = () => {
for (const el of triggerEls) {
let cur = el;
let depth = 0;
while (cur && depth < 3) {
try { dispatchRealClick(cur); } catch(e) {}
cur = cur.parentElement;
depth++;
}
}
};
fireClicks();
let pollCount = 0;
const MAX_POLLS = 28;
const POLL_INTERVAL = 250;
const RE_ACTION_EVERY = 2;
const tryExtract = () => {
if (finalized) return;
let newLinks = [];
try { newLinks = extractChapterLinksFromDoc(doc, indexUrl); } catch(e) {}
if (newLinks.length > initialLinks.length && newLinks.length >= 5) {
if (iframe && iframe.parentNode) iframe.remove();
clearTimeout(hardTimeout);
finalize(newLinks);
return;
}
pollCount++;
if (pollCount < MAX_POLLS) {
if (pollCount % RE_ACTION_EVERY === 0) fireClicks();
setTimeout(tryExtract, POLL_INTERVAL);
} else {
if (iframe && iframe.parentNode) iframe.remove();
clearTimeout(hardTimeout);
finalize(newLinks.length > initialLinks.length ? newLinks : initialLinks);
}
};
setTimeout(tryExtract, POLL_INTERVAL);
}
const firstIframe = document.createElement('iframe');
firstIframe.style.cssText = 'position:absolute;width:0;height:0;border:none;visibility:hidden;';
firstIframe.src = indexUrl;
let firstLoaded = false;
const firstTimeout = setTimeout(() => {
if (!firstLoaded) {
if (firstIframe.parentNode) firstIframe.remove();
clearTimeout(hardTimeout);
finalize([]);
}
}, 15000);
firstIframe.onload = () => {
firstLoaded = true;
clearTimeout(firstTimeout);
const result = safeGetIframeDoc(firstIframe);
if (!result.doc) {
if (firstIframe.parentNode) firstIframe.remove();
clearTimeout(hardTimeout);
finalize([]);
return;
}
const doc = result.doc;
let firstLinks = [];
try { firstLinks = extractChapterLinksFromDoc(doc, indexUrl); } catch(e) {}
const pagerUrls = findPagerUrls(doc, indexUrl);
if (pagerUrls.length > 0) {
if (firstIframe.parentNode) firstIframe.remove();
runPagerStrategy(pagerUrls, firstLinks);
return;
}
const { needsExpand, triggerEls } = detectExpandNeed(doc);
if (needsExpand && triggerEls.length > 0 && firstLinks.length < 30) {
runExpandStrategy(doc, triggerEls, firstIframe, firstLinks);
return;
}
if (firstIframe.parentNode) firstIframe.remove();
clearTimeout(hardTimeout);
finalize(firstLinks);
};
document.body.appendChild(firstIframe);
}
function preloadNextChapter(url) {
if (!url) return;
const cacheKey = normalizeUrl(url);
if (chapterCache.has(cacheKey)) return;
if (currentPreloadTask && normalizeUrl(currentPreloadTask.url) === cacheKey) return;
cancelCurrentPreload();
const task = {
url: url,
iframe: null,
timeout: null,
pollTimeout: null,
cancelled: false
};
currentPreloadTask = task;
const iframe = document.createElement('iframe');
iframe.style.cssText = 'position:absolute;width:0;height:0;border:none;visibility:hidden;';
iframe.src = url;
document.body.appendChild(iframe);
task.iframe = iframe;
const cleanup = () => {
if (task.iframe && task.iframe.parentNode) task.iframe.remove();
if (task.timeout) clearTimeout(task.timeout);
if (task.pollTimeout) clearTimeout(task.pollTimeout);
task.iframe = null;
task.timeout = null;
task.pollTimeout = null;
if (currentPreloadTask === task) currentPreloadTask = null;
};
let loaded = false;
task.timeout = setTimeout(() => {
if (!loaded) cleanup();
}, 15000);
iframe.onload = function() {
loaded = true;
if (task.timeout) { clearTimeout(task.timeout); task.timeout = null; }
if (task.cancelled) { cleanup(); return; }
const result = safeGetIframeDoc(iframe);
if (!result.doc) { cleanup(); return; }
let pollCount = 0;
const MAX_POLL_COUNT = 15;
const POLL_INTERVAL = 200;
const tryExtract = () => {
if (task.cancelled) { cleanup(); return; }
let article = null;
try { article = extractContent(result.doc); } catch(e) {}
if (article) {
chapterCache.set(cacheKey, { article, timestamp: Date.now() });
if (chapterCache.size > 2) {
const firstKey = chapterCache.keys().next().value;
chapterCache.delete(firstKey);
}
cleanup();
return;
}
pollCount++;
if (pollCount < MAX_POLL_COUNT) {
task.pollTimeout = setTimeout(tryExtract, POLL_INTERVAL);
} else {
cleanup();
}
};
tryExtract();
};
}
function cancelCurrentPreload() {
if (!currentPreloadTask) return;
const task = currentPreloadTask;
task.cancelled = true;
if (task.iframe && task.iframe.parentNode) task.iframe.remove();
if (task.timeout) clearTimeout(task.timeout);
if (task.pollTimeout) clearTimeout(task.pollTimeout);
currentPreloadTask = null;
}
function schedulePreloadNext(overlay) {
if (preloadScheduleTimer) {
clearTimeout(preloadScheduleTimer);
preloadScheduleTimer = null;
}
preloadScheduleTimer = setTimeout(() => {
preloadScheduleTimer = null;
const nav = overlay.__nav;
if (!nav || !nav.next) return;
preloadNextChapter(nav.next.href);
}, 500);
}
function updateProgressBar(overlay, targetUrl) {
const thumb = overlay.querySelector('#reader-progress-thumb');
if (!thumb) return;
const container = thumb.parentNode;
const canDrag = catalogCache && catalogCache.links && catalogCache.links.length > 1;
if (container) {
container.style.cursor = canDrag ? 'pointer' : 'default';
container.style.opacity = canDrag ? '1' : '0.5';
}
if (catalogCacheLoading) return;
if (!catalogCache || !catalogCache.links || !catalogCache.links.length) {
thumb.style.left = 'calc(100% - 7px)';
return;
}
const total = catalogCache.links.length;
if (total <= 1) {
thumb.style.left = 'calc(100% - 7px)';
return;
}
const url = targetUrl || window.location.href;
const currentKey = normalizeUrl(url);
let currentIdx = -1;
for (let i = 0; i < total; i++) {
const l = catalogCache.links[i];
if (!l || l.__missing || !l.href) continue;
if (normalizeUrl(l.href) === currentKey) {
currentIdx = i;
break;
}
}
if (currentIdx !== -1) {
const percent = (currentIdx / (total - 1)) * 100;
thumb.style.left = `calc(${percent}% - 7px)`;
return;
}
const extractNum = (u) => {
try {
const p = new URL(u).pathname;
const m = p.match(/\/(\d+)\.html?$/i) || p.match(/(\d+)(?=\.html?$)/i);
return m ? parseInt(m[1], 10) : null;
} catch(e) { return null; }
};
const curNum = extractNum(url);
let maxNum = 0;
for (const l of catalogCache.links) {
if (!l || l.__missing || !l.href) continue;
const n = extractNum(l.href);
if (n !== null && n > maxNum) maxNum = n;
}
if (curNum !== null && maxNum > 0 && curNum <= maxNum) {
const percent = (curNum / maxNum) * 100;
thumb.style.left = `calc(${percent}% - 7px)`;
return;
}
thumb.style.left = 'calc(0% - 7px)';
}
function removeCatalogImmediately(overlay) {
if (currentCatalogView) {
if (currentCatalogView.__pollInterval) {
clearInterval(currentCatalogView.__pollInterval);
currentCatalogView.__pollInterval = null;
}
currentCatalogView.remove();
currentCatalogView = null;
}
const mask = overlay.querySelector('#reader-catalog-mask');
if (mask) mask.remove();
setIconBtnActive(overlay, 'catalog', false);
}
function removeSettingsImmediately(overlay) {
const panel = overlay.querySelector('#reader-settings-view');
const mask = overlay.querySelector('#reader-settings-mask');
if (panel) panel.remove();
if (mask) mask.remove();
setIconBtnActive(overlay, 'settings', false);
}
function renderCatalogLinks(container, links, colors, onLinkClick, currentUrl, isNight, order) {
container.innerHTML = '';
if (!links.length) {
const errorDiv = document.createElement('div');
errorDiv.style.cssText = `padding: 30px; text-align: center; color: ${colors.subtleTextColor}; font-size: 14px;`;
errorDiv.textContent = '未找到章节链接。';
container.appendChild(errorDiv);
return;
}
const curKey = normalizeUrl(currentUrl || window.location.href);
const currentBgHighlight = isNight ? 'rgba(255,255,255,0.08)' : 'rgba(0,123,255,0.08)';
const displayLinks = (order === 'desc') ? links.slice().reverse() : links;
displayLinks.forEach(link => {
const isMissing = link.__missing === true;
const isCurrent = !isMissing && link.href && normalizeUrl(link.href) === curKey;
if (isMissing) {
const div = document.createElement('div');
div.dataset.missing = 'true';
div.style.cssText = `
display: flex; align-items: center; gap: 8px;
padding: 12px 8px 12px 12px;
color: ${colors.subtleTextColor};
font-size: 15px;
border-bottom: 1px solid ${colors.borderColor};
background: transparent;
line-height: 1.5;
font-style: italic;
opacity: 0.62;
cursor: not-allowed;
-webkit-tap-highlight-color: transparent;
`;
const iconSpan = document.createElement('span');
iconSpan.innerHTML = ICONS.missing;
iconSpan.style.cssText = `flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
color: ${colors.subtleTextColor};`;
div.appendChild(iconSpan);
const textSpan = document.createElement('span');
textSpan.textContent = link.text;
textSpan.style.cssText = 'flex: 1; overflow: hidden; text-overflow: ellipsis; text-decoration: line-through; text-decoration-thickness: 1px;';
div.appendChild(textSpan);
const tag = document.createElement('span');
tag.textContent = '缺失';
tag.style.cssText = `flex-shrink: 0; font-style: normal; font-size: 11px;
color: ${colors.subtleTextColor};
border: 1px solid ${colors.borderColor};
border-radius: 4px; padding: 1px 6px; opacity: 0.8;`;
div.appendChild(tag);
div.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
notify('此章在原站缺失,无法阅读');
});
container.appendChild(div);
return;
}
const a = document.createElement('a');
a.href = link.href;
if (isCurrent) a.dataset.current = 'true';
a.style.cssText = `
display: flex; align-items: center; gap: 8px;
padding: 12px 8px 12px 12px;
color: ${isCurrent ? colors.navBtnColor : colors.textColor};
text-decoration: none;
font-size: 15px; font-weight: ${isCurrent ? '600' : 'normal'};
border-bottom: 1px solid ${colors.borderColor};
background: ${isCurrent ? currentBgHighlight : 'transparent'};
line-height: 1.5; transition: background 0.2s;
-webkit-tap-highlight-color: transparent;
`;
if (isCurrent) {
const cross = document.createElement('span');
cross.className = 'reader-current-crosshair';
cross.innerHTML = ICONS.crosshair;
cross.style.cssText = `flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
color: ${colors.navBtnColor};`;
a.appendChild(cross);
}
const textSpan = document.createElement('span');
textSpan.textContent = link.text;
textSpan.style.cssText = 'flex: 1; overflow: hidden; text-overflow: ellipsis;';
a.appendChild(textSpan);
a.addEventListener('mouseenter', () => a.style.background = colors.controlBgColor);
a.addEventListener('mouseleave', () => {
if (isCurrent) a.style.background = currentBgHighlight;
else a.style.background = 'transparent';
});
if (onLinkClick) {
a.addEventListener('click', (e) => {
e.preventDefault();
onLinkClick(link.href);
});
}
container.appendChild(a);
});
const currentEl = container.querySelector('a[data-current="true"]');
if (currentEl && order === 'asc') {
setTimeout(() => {
try {
const listRect = container.getBoundingClientRect();
const elRect = currentEl.getBoundingClientRect();
container.scrollTop += (elRect.top - listRect.top);
} catch(e) {
try { currentEl.scrollIntoView({ block: 'start', behavior: 'auto' }); } catch(e2) {}
}
}, 50);
}
}
function refreshCatalogColors(userBg, isNight) {
if (!currentCatalogView) return;
const actualBg = getEffectiveBgColor(userBg, isNight);
currentCatalogView.style.backgroundColor = actualBg;
const catalogColors = calculateColors(userBg, isNight);
const currentBgHighlight = isNight ? 'rgba(255,255,255,0.08)' : 'rgba(0,123,255,0.08)';
currentCatalogView.querySelectorAll('a').forEach(a => {
const isCurrent = a.dataset.current === 'true';
if (isCurrent) {
a.style.color = catalogColors.navBtnColor;
a.style.background = currentBgHighlight;
a.style.borderBottomColor = catalogColors.borderColor;
} else {
a.style.color = catalogColors.textColor;
a.style.background = 'transparent';
a.style.borderBottomColor = catalogColors.borderColor;
}
const cross = a.querySelector('.reader-current-crosshair');
if (cross) cross.style.color = catalogColors.navBtnColor;
});
currentCatalogView.querySelectorAll('[data-missing="true"]').forEach(div => {
div.style.color = catalogColors.subtleTextColor;
div.style.borderBottomColor = catalogColors.borderColor;
const iconSpan = div.querySelector('span');
if (iconSpan) iconSpan.style.color = catalogColors.subtleTextColor;
const tag = div.querySelector('span:last-child');
if (tag) {
tag.style.color = catalogColors.subtleTextColor;
tag.style.borderColor = catalogColors.borderColor;
}
});
const locateBtn = currentCatalogView.querySelector('#reader-catalog-locate');
if (locateBtn) {
locateBtn.style.background = catalogColors.controlBgColor;
locateBtn.style.color = catalogColors.navBtnColor;
locateBtn.style.borderColor = catalogColors.borderColor;
}
const sortBtn = currentCatalogView.querySelector('.reader-catalog-sort-btn');
if (sortBtn) {
sortBtn.style.background = 'transparent';
sortBtn.style.color = catalogColors.subtleTextColor;
}
const toolbarLabel = currentCatalogView.querySelector('.reader-catalog-toolbar-label');
if (toolbarLabel) {
toolbarLabel.style.color = catalogColors.subtleTextColor;
}
const headerSpan = currentCatalogView.querySelector('div > span');
if (headerSpan) headerSpan.style.color = catalogColors.textColor;
}
function showCatalog(indexUrl, overlay, onChapterNav) {
const existingCatalog = overlay.querySelector('#reader-catalog-view');
if (existingCatalog) {
if (typeof existingCatalog.__doClose === 'function') existingCatalog.__doClose();
return;
}
removeSettingsImmediately(overlay);
setIconBtnActive(overlay, 'catalog', true);
const isNight = overlay.__isNight === true;
const userBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor);
const actualBg = getEffectiveBgColor(userBg, isNight);
const colors = calculateColors(userBg, isNight);
const immersiveBottom = overlay.querySelector('#reader-immersive-bottom');
if (immersiveBottom) {
immersiveBottom.classList.add('above-drawer');
applyImmersiveBottomStyle(immersiveBottom, colors, immersiveBottom.dataset.style, true);
}
const mask = document.createElement('div');
mask.id = 'reader-catalog-mask';
mask.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); z-index: 1000; opacity: 0; transition: opacity 0.3s; touch-action: none;';
overlay.appendChild(mask);
const catalogView = document.createElement('div');
catalogView.id = 'reader-catalog-view';
catalogView.style.cssText = `position: fixed; bottom: 0; left: 0; width: 100%; height: 78%; background-color: ${actualBg}; z-index: 1001; border-radius: 16px 16px 0 0; display: flex; flex-direction: column; transform: translateY(100%); transition: transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1); box-sizing: border-box; box-shadow: 0 -4px 12px rgba(0,0,0,0.15); overflow: hidden; overscroll-behavior: contain;`;
const header = document.createElement('div');
header.style.cssText = `display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-bottom: 1px solid ${colors.borderColor}; flex-shrink: 0;`;
const headerTitle = document.createElement('span');
headerTitle.style.cssText = `font-size: 16px; font-weight: bold; color: ${colors.textColor};`;
headerTitle.textContent = '目录';
const closeBtn = document.createElement('span');
closeBtn.textContent = '收起';
closeBtn.style.cssText = `font-size: 14px; color: ${colors.subtleTextColor}; cursor: pointer; padding: 5px; font-weight: normal; -webkit-tap-highlight-color: transparent;`;
header.appendChild(headerTitle);
header.appendChild(closeBtn);
const doClose = () => {
if (catalogView.__closing) return;
catalogView.__closing = true;
setIconBtnActive(overlay, 'catalog', false);
if (catalogView.__pollInterval) {
clearInterval(catalogView.__pollInterval);
catalogView.__pollInterval = null;
}
catalogView.style.transform = 'translateY(100%)';
mask.style.opacity = '0';
setTimeout(() => {
catalogView.remove();
mask.remove();
if (currentCatalogView === catalogView) currentCatalogView = null;
if (overlay.querySelector('#reader-catalog-view') || overlay.querySelector('#reader-settings-view')) {
return;
}
if (immersiveBottom) {
immersiveBottom.classList.remove('above-drawer');
const restoreIsNight = overlay.__isNight === true;
const restoreUserBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor);
const restoreColors = calculateColors(restoreUserBg, restoreIsNight);
applyImmersiveBottomStyle(
immersiveBottom,
restoreColors,
immersiveBottom.dataset.style,
immersiveBottom.dataset.visible === 'true'
);
}
}, 300);
};
catalogView.__doClose = doClose;
closeBtn.addEventListener('click', doClose);
mask.addEventListener('click', doClose);
catalogView.appendChild(header);
const toolbar = document.createElement('div');
toolbar.className = 'reader-catalog-toolbar';
toolbar.style.cssText = `display: flex; justify-content: space-between; align-items: center;
padding: 8px 20px 6px; flex-shrink: 0;
border-bottom: 1px solid ${colors.borderColor};`;
const toolbarLabel = document.createElement('span');
toolbarLabel.className = 'reader-catalog-toolbar-label';
toolbarLabel.textContent = '';
toolbarLabel.style.cssText = `font-size: 12px; color: ${colors.subtleTextColor}; user-select: none;`;
function updateToolbarCount(links) {
if (links && links.length > 0) {
let missingCount = 0;
for (const l of links) if (l && l.__missing === true) missingCount++;
let text = `共 ${links.length} 章`;
if (missingCount > 0) {
text += `(其中 ${missingCount} 章原站缺失)`;
}
toolbarLabel.textContent = text;
} else {
toolbarLabel.textContent = '';
}
}
const sortBtn = document.createElement('div');
sortBtn.className = 'reader-catalog-sort-btn';
sortBtn.title = '切换正序/倒序';
sortBtn.style.cssText = `display: inline-flex; align-items: center; gap: 5px;
padding: 5px 8px; border-radius: 6px;
font-size: 12px; font-weight: 500; line-height: 1;
color: ${colors.subtleTextColor};
background: transparent;
border: none;
cursor: pointer; user-select: none;
-webkit-tap-highlight-color: transparent;
transition: transform 0.12s ease;`;
function updateSortBtn() {
const isAsc = catalogOrder === 'asc';
const label = isAsc ? '倒序' : '正序';
const icon = isAsc ? ICONS.catalogSortDesc : ICONS.catalogSortAsc;
sortBtn.innerHTML = icon + `${label}`;
}
updateSortBtn();
sortBtn.addEventListener('mousedown', () => {
sortBtn.style.transform = 'scale(0.95)';
});
sortBtn.addEventListener('mouseup', () => {
sortBtn.style.transform = '';
});
sortBtn.addEventListener('mouseleave', () => {
sortBtn.style.transform = '';
});
sortBtn.addEventListener('click', (e) => {
e.stopPropagation();
catalogOrder = catalogOrder === 'asc' ? 'desc' : 'asc';
updateSortBtn();
if (catalogCache && catalogCache.url === indexUrl) {
try {
renderCatalogLinks(listContainer, catalogCache.links, colors, handleLinkClick, overlay.__currentUrl || window.location.href, isNight, catalogOrder);
listContainer.scrollTop = 0;
scheduleCheck();
} catch(err) {}
}
});
toolbar.appendChild(toolbarLabel);
toolbar.appendChild(sortBtn);
catalogView.appendChild(toolbar);
const listContainer = document.createElement('div');
listContainer.id = 'reader-catalog-list';
listContainer.style.cssText = 'flex: 1; overflow-y: auto; padding: 10px 20px 100px; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;';
catalogView.appendChild(listContainer);
const locateBtn = document.createElement('div');
locateBtn.id = 'reader-catalog-locate';
locateBtn.className = 'reader-locate-btn';
locateBtn.title = '定位到当前章节';
locateBtn.innerHTML = ICONS.crosshair;
locateBtn.style.cssText = `position: absolute; right: 16px; bottom: 80px;
width: 40px; height: 40px; border-radius: 50%;
background: ${colors.controlBgColor}; color: ${colors.navBtnColor};
border: 1px solid ${colors.borderColor};
display: flex; align-items: center; justify-content: center;
cursor: pointer; z-index: 5;
box-shadow: 0 2px 10px rgba(0,0,0,0.18);
-webkit-tap-highlight-color: transparent; outline: none;
touch-action: none;`;
locateBtn.addEventListener('click', (e) => {
e.stopPropagation();
const currentEl = listContainer.querySelector('a[data-current="true"]');
if (currentEl) {
try {
const listRect = listContainer.getBoundingClientRect();
const elRect = currentEl.getBoundingClientRect();
listContainer.scrollTo({
top: listContainer.scrollTop + (elRect.top - listRect.top),
behavior: 'smooth'
});
} catch(e) {
currentEl.scrollIntoView();
}
} else {
notify('未找到当前章节');
}
});
catalogView.appendChild(locateBtn);
function updateLocateBtnVisibility() {
const currentEl = listContainer.querySelector('a[data-current="true"]');
if (!currentEl) {
locateBtn.classList.remove('reader-locate-visible');
return;
}
const listRect = listContainer.getBoundingClientRect();
const elRect = currentEl.getBoundingClientRect();
const isCompletelyOut = elRect.bottom <= listRect.top || elRect.top >= listRect.bottom;
locateBtn.classList.toggle('reader-locate-visible', isCompletelyOut);
}
catalogView.__updateLocateBtn = updateLocateBtnVisibility;
listContainer.addEventListener('scroll', updateLocateBtnVisibility, { passive: true });
overlay.appendChild(catalogView);
currentCatalogView = catalogView;
requestAnimationFrame(() => {
catalogView.style.transform = 'translateY(0)';
mask.style.opacity = '1';
});
const handleLinkClick = (url) => {
doClose();
if (onChapterNav) onChapterNav(url);
else window.location.href = url;
};
const curUrl = overlay.__currentUrl || window.location.href;
const scheduleCheck = () => {
setTimeout(() => {
try { updateLocateBtnVisibility(); } catch(e) {}
}, 200);
};
if (catalogCache && catalogCache.url === indexUrl) {
updateToolbarCount(catalogCache.links);
renderCatalogLinks(listContainer, catalogCache.links, colors, handleLinkClick, curUrl, isNight, catalogOrder);
scheduleCheck();
return;
}
if (catalogCacheLoading && catalogCacheLoading.url === indexUrl) {
const loadingDiv = document.createElement('div');
loadingDiv.id = 'reader-catalog-loading';
loadingDiv.style.cssText = `padding: 30px; text-align: center; color: ${colors.subtleTextColor}; font-size: 14px;`;
loadingDiv.textContent = '正在加载目录...';
listContainer.appendChild(loadingDiv);
const check = setInterval(() => {
if (catalogCache && catalogCache.url === indexUrl) {
clearInterval(check);
catalogView.__pollInterval = null;
const el = overlay.querySelector('#reader-catalog-loading');
if (el) el.remove();
updateToolbarCount(catalogCache.links);
renderCatalogLinks(listContainer, catalogCache.links, colors, handleLinkClick, curUrl, isNight, catalogOrder);
scheduleCheck();
} else if (!catalogCacheLoading) {
clearInterval(check);
catalogView.__pollInterval = null;
const el = overlay.querySelector('#reader-catalog-loading');
if (el) el.remove();
renderError(listContainer, '目录预加载失败,请稍后重试。', indexUrl, colors);
}
}, 150);
catalogView.__pollInterval = check;
return;
}
const loadingDiv = document.createElement('div');
loadingDiv.id = 'reader-catalog-loading';
loadingDiv.style.cssText = `padding: 30px; text-align: center; color: ${colors.subtleTextColor}; font-size: 14px;`;
loadingDiv.textContent = '正在加载目录...';
listContainer.appendChild(loadingDiv);
const iframe = document.createElement('iframe');
iframe.style.cssText = 'position: absolute; width: 0; height: 0; border: none; visibility: hidden;';
iframe.src = indexUrl;
document.body.appendChild(iframe);
let iframeLoaded = false;
const timeoutFallback = setTimeout(() => {
if (!iframeLoaded) {
iframe.remove();
const el = overlay.querySelector('#reader-catalog-loading');
if (el) {
el.remove();
renderError(listContainer, '目录加载超时,可能是目标网站禁止嵌入(iframe)或网络问题。', indexUrl, colors);
}
}
}, 8000);
iframe.onload = function() {
iframeLoaded = true;
clearTimeout(timeoutFallback);
const el = overlay.querySelector('#reader-catalog-loading');
if (el) el.remove();
const result = safeGetIframeDoc(iframe);
if (!result.doc) {
if (result.error === 'cross-origin') {
renderError(listContainer, '目录页禁止跨域访问(iframe 被同源策略或 X-Frame-Options 拦截)。', indexUrl, colors);
} else {
renderError(listContainer, '无法访问目录页内容。', indexUrl, colors);
}
iframe.remove();
return;
}
try {
const links = extractChapterLinksFromDoc(result.doc, indexUrl);
if (links.length === 0) {
renderError(listContainer, '未能从目录页提取到章节链接。', indexUrl, colors);
return;
}
catalogCache = { url: indexUrl, links };
// console.log(`[阅读模式] 目录抓取成功,共 ${links.length} 条章节链接`);
overlay.dispatchEvent(new CustomEvent('reader-catalog-ready'));
updateToolbarCount(links);
renderCatalogLinks(listContainer, links, colors, handleLinkClick, curUrl, isNight, catalogOrder);
scheduleCheck();
} catch (e) {
// console.error('解析 iframe 目录失败:', e);
renderError(listContainer, '解析目录失败。', indexUrl, colors);
} finally {
iframe.remove();
}
};
function renderError(container, msg, url, c) {
const errorDiv = document.createElement('div');
errorDiv.style.cssText = `padding: 20px; text-align: center; color: ${c.subtleTextColor}; font-size: 14px;`;
errorDiv.textContent = msg;
const jumpBtn = document.createElement('button');
jumpBtn.textContent = '点此手动打开目录页';
jumpBtn.style.cssText = `margin-top: 15px; padding: 10px 20px; background: ${c.navBtnColor}; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; -webkit-tap-highlight-color: transparent; outline: none;`;
jumpBtn.addEventListener('click', () => { window.open(url, '_blank'); });
errorDiv.appendChild(document.createElement('br'));
errorDiv.appendChild(jumpBtn);
container.appendChild(errorDiv);
}
}
function showSettingsPanel(overlay, opts) {
const existingSettings = overlay.querySelector('#reader-settings-view');
if (existingSettings) {
if (typeof existingSettings.__doClose === 'function') existingSettings.__doClose();
return;
}
removeCatalogImmediately(overlay);
setIconBtnActive(overlay, 'settings', true);
const isNight = overlay.__isNight === true;
const userBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor);
const actualBg = getEffectiveBgColor(userBg, isNight);
const colors = calculateColors(userBg, isNight);
const immersiveBottom = overlay.querySelector('#reader-immersive-bottom');
if (immersiveBottom) {
immersiveBottom.classList.add('above-drawer');
applyImmersiveBottomStyle(immersiveBottom, colors, immersiveBottom.dataset.style, true);
}
const mask = document.createElement('div');
mask.id = 'reader-settings-mask';
mask.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); z-index: 1000; opacity: 0; transition: opacity 0.3s; touch-action: none;';
overlay.appendChild(mask);
const panel = document.createElement('div');
panel.id = 'reader-settings-view';
panel.style.cssText = `position: fixed; bottom: 0; left: 0; width: 100%;
background-color: ${actualBg}; z-index: 1001;
border-radius: 16px 16px 0 0; display: flex; flex-direction: column;
transform: translateY(100%); transition: transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1);
box-sizing: border-box; box-shadow: 0 -4px 12px rgba(0,0,0,0.15);
padding: 0 0 env(safe-area-inset-bottom, 0px);
max-height: 90%;
overscroll-behavior: contain;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;`;
const header = document.createElement('div');
header.style.cssText = `display: flex; justify-content: space-between; align-items: center;
padding: 14px 18px; border-bottom: 1px solid ${colors.borderColor}; flex-shrink: 0;`;
const headerLeft = document.createElement('div');
headerLeft.style.cssText = 'display: flex; align-items: center; gap: 10px;';
const titleSpan = document.createElement('span');
titleSpan.textContent = '设置';
titleSpan.style.cssText = `font-size: 15px; font-weight: bold; color: ${colors.textColor};`;
headerLeft.appendChild(titleSpan);
const closeBtn = document.createElement('span');
closeBtn.textContent = '收起';
closeBtn.style.cssText = `font-size: 13px; color: ${colors.subtleTextColor}; cursor: pointer; padding: 5px; -webkit-tap-highlight-color: transparent;`;
const doClose = () => {
if (panel.__closing) return;
panel.__closing = true;
setIconBtnActive(overlay, 'settings', false);
panel.style.transform = 'translateY(100%)';
mask.style.opacity = '0';
setTimeout(() => {
panel.remove();
mask.remove();
if (overlay.querySelector('#reader-catalog-view') || overlay.querySelector('#reader-settings-view')) {
return;
}
if (immersiveBottom) {
immersiveBottom.classList.remove('above-drawer');
const restoreIsNight = overlay.__isNight === true;
const restoreUserBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor);
const restoreColors = calculateColors(restoreUserBg, restoreIsNight);
applyImmersiveBottomStyle(
immersiveBottom,
restoreColors,
immersiveBottom.dataset.style,
immersiveBottom.dataset.visible === 'true'
);
}
}, 300);
};
panel.__doClose = doClose;
closeBtn.addEventListener('click', doClose);
mask.addEventListener('click', doClose);
header.appendChild(headerLeft);
header.appendChild(closeBtn);
panel.appendChild(header);
const body = document.createElement('div');
body.style.cssText = 'flex: 1; overflow-y: auto; padding: 16px 18px 90px; display: flex; flex-direction: column; gap: 14px; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;';
panel.appendChild(body);
function renderMainView() {
titleSpan.textContent = '设置';
body.innerHTML = '';
const fontSizeRow = document.createElement('div');
fontSizeRow.style.cssText = `display: flex; align-items: center; gap: 10px;`;
const fontSizeLabel = document.createElement('div');
fontSizeLabel.textContent = '字号';
fontSizeLabel.style.cssText = `font-size: 13px; color: ${colors.subtleTextColor}; flex-shrink: 0; width: 40px;`;
fontSizeRow.appendChild(fontSizeLabel);
const sizeBtns = document.createElement('div');
sizeBtns.style.cssText = `display: flex; align-items: center; flex: 1;
background: ${colors.controlBgColor}; border-radius: 8px;
border: 1px solid ${colors.borderColor}; overflow: hidden;`;
const btnMinus = document.createElement('div');
btnMinus.textContent = 'A-';
btnMinus.style.cssText = `flex: 1; text-align: center; padding: 8px 0; cursor: pointer;
font-size: 14px; color: ${colors.textColor}; user-select: none;
-webkit-tap-highlight-color: transparent;`;
const sizeLabel = document.createElement('div');
sizeLabel.textContent = parseInt(gmGet(STORAGE_KEYS.FONT_SIZE, DEFAULT_SETTINGS.fontSize), 10) || 18;
sizeLabel.style.cssText = `flex: 1; text-align: center; font-size: 13px;
color: ${colors.textColor}; font-weight: 600; border-left: 1px solid ${colors.borderColor};
border-right: 1px solid ${colors.borderColor}; padding: 8px 0;`;
const btnPlus = document.createElement('div');
btnPlus.textContent = 'A+';
btnPlus.style.cssText = btnMinus.style.cssText;
btnMinus.addEventListener('mouseenter', () => btnMinus.style.background = colors.borderColor);
btnMinus.addEventListener('mouseleave', () => btnMinus.style.background = 'transparent');
btnPlus.addEventListener('mouseenter', () => btnPlus.style.background = colors.borderColor);
btnPlus.addEventListener('mouseleave', () => btnPlus.style.background = 'transparent');
btnMinus.addEventListener('click', () => {
const cur = parseInt(gmGet(STORAGE_KEYS.FONT_SIZE, DEFAULT_SETTINGS.fontSize), 10) || 18;
const next = Math.max(11, cur - 1);
opts.onFontSizeChange(next + 'px');
sizeLabel.textContent = next;
});
btnPlus.addEventListener('click', () => {
const cur = parseInt(gmGet(STORAGE_KEYS.FONT_SIZE, DEFAULT_SETTINGS.fontSize), 10) || 18;
const next = Math.min(30, cur + 1);
opts.onFontSizeChange(next + 'px');
sizeLabel.textContent = next;
});
sizeBtns.appendChild(btnMinus);
sizeBtns.appendChild(sizeLabel);
sizeBtns.appendChild(btnPlus);
fontSizeRow.appendChild(sizeBtns);
body.appendChild(fontSizeRow);
const bgRow = document.createElement('div');
bgRow.style.cssText = `display: flex; align-items: center; gap: 10px;`;
const bgLabel = document.createElement('div');
bgLabel.textContent = '背景';
bgLabel.style.cssText = `font-size: 13px; color: ${colors.subtleTextColor}; flex-shrink: 0; width: 40px;`;
bgRow.appendChild(bgLabel);
const swatchRow = document.createElement('div');
swatchRow.style.cssText = `display: flex; align-items: center; gap: 10px; flex: 1;`;
const activeBg = gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor);
Object.keys(config.backgroundOptions).forEach(name => {
const color = config.backgroundOptions[name];
const isSelected = color === activeBg;
const swatch = document.createElement('div');
swatch.title = name;
swatch.dataset.color = color;
swatch.style.cssText = `width: 32px; height: 32px; border-radius: 50%;
background: ${color}; cursor: pointer; position: relative;
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
border: 2px solid ${isSelected ? colors.navBtnColor : 'transparent'};
transition: transform 0.2s; box-sizing: border-box;
-webkit-tap-highlight-color: transparent;`;
swatch.addEventListener('mouseenter', () => swatch.style.transform = 'scale(1.1)');
swatch.addEventListener('mouseleave', () => swatch.style.transform = 'scale(1)');
swatch.addEventListener('click', () => {
opts.onBgChange(color);
panel.style.background = color;
swatchRow.querySelectorAll('div').forEach(el => { el.style.border = '2px solid transparent'; });
swatch.style.border = `2px solid ${colors.navBtnColor}`;
});
swatchRow.appendChild(swatch);
});
bgRow.appendChild(swatchRow);
body.appendChild(bgRow);
}
renderMainView();
overlay.appendChild(panel);
requestAnimationFrame(() => {
panel.style.transform = 'translateY(0)';
mask.style.opacity = '1';
});
}
function createNavBar(nav, colors, addHash = false, id = '', onCatalogClick, onChapterNav) {
if (!nav.prev && !nav.next && !nav.index) return null;
const bar = document.createElement('div');
if (id) bar.id = id;
bar.className = 'reader-nav-bar';
bar.style.cssText = 'display:flex;justify-content:center;gap:20px;margin:20px 0;font-size:15px;';
const createBtn = (text, link) => {
const a = document.createElement('a');
a.textContent = text;
let finalHref = link ? link.href : '#';
if (link && addHash) {
finalHref = link.href.startsWith('javascript:') ? link.href : addReaderModeHash(link.href);
}
a.href = finalHref;
a.style.cssText = `color:${link ? colors.navBtnColor : '#999'};text-decoration:none;cursor:${link ? 'pointer' : 'default'};opacity:${link ? 1 : 0.5};background:none;font-weight:normal;-webkit-tap-highlight-color:transparent;`;
if (!link) {
a.addEventListener('click', e => e.preventDefault());
} else if (text === '目录' && onCatalogClick) {
a.addEventListener('click', e => {
e.preventDefault();
onCatalogClick(link.href);
});
} else if (onChapterNav) {
a.addEventListener('click', e => {
e.preventDefault();
onChapterNav(finalHref);
});
}
return a;
};
bar.appendChild(createBtn('上一页', nav.prev));
bar.appendChild(createBtn('目录', nav.index));
bar.appendChild(createBtn('下一页', nav.next));
return bar;
}
function applyImmersiveTopStyle(el, colors, style, visible) {
if (!el) return;
const transform = visible ? 'translateY(0)' : 'translateY(-100%)';
const opacity = visible ? '1' : '0';
const borderBottom = visible ? colors.borderColor : 'transparent';
const boxShadow = visible ? '0 2px 8px rgba(0,0,0,0.06)' : 'none';
el.style.cssText = `position: fixed; z-index: 90;
display: flex; align-items: center;
background: ${colors.controlBgColor};
transition: transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.32s cubic-bezier(0.2, 0.8, 0.2, 1), background 0.3s ease, border-color 0.3s ease, border-radius 0.3s ease, box-shadow 0.3s ease;
transform: ${transform};
opacity: ${opacity};
pointer-events: ${visible ? 'auto' : 'none'};
-webkit-tap-highlight-color: transparent; outline: none;
touch-action: none; overscroll-behavior: contain;
color: ${colors.textColor};
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
top: 0; left: 0; right: 0;
padding: 10px 14px;
border-bottom: 1px solid ${borderBottom};
border-radius: 0;
box-shadow: ${boxShadow};`;
}
function applyImmersiveBottomStyle(el, colors, style, visible) {
if (!el) return;
const aboveDrawer = el.classList.contains('above-drawer');
const effectiveVisible = aboveDrawer ? true : visible;
const zIndex = aboveDrawer ? 1010 : 90;
const transform = effectiveVisible ? 'translateY(0)' : 'translateY(100%)';
const opacity = effectiveVisible ? '1' : '0';
const borderTop = effectiveVisible ? colors.borderColor : 'transparent';
const boxShadow = effectiveVisible ? '0 -2px 8px rgba(0,0,0,0.06)' : 'none';
el.style.cssText = `position: fixed; z-index: ${zIndex};
display: flex; flex-direction: column;
background: ${colors.controlBgColor};
transition: transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.32s cubic-bezier(0.2, 0.8, 0.2, 1), background 0.3s ease, border-color 0.3s ease, border-radius 0.3s ease, box-shadow 0.3s ease;
transform: ${transform};
opacity: ${opacity};
pointer-events: ${effectiveVisible ? 'auto' : 'none'};
-webkit-tap-highlight-color: transparent; outline: none;
touch-action: none; overscroll-behavior: contain;
color: ${colors.textColor};
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
bottom: 0; left: 0; right: 0;
padding: 0 0 env(safe-area-inset-bottom, 0px);
border-top: 1px solid ${borderTop};
border-radius: 0;
box-shadow: ${boxShadow};`;
}
function refreshImmersiveColors(container, colors, readingBg) {
if (!container) return;
const style = container.dataset.style || 'edge';
const visible = container.dataset.visible === 'true';
if (container.id === 'reader-immersive-top') {
applyImmersiveTopStyle(container, colors, style, visible);
} else {
applyImmersiveBottomStyle(container, colors, style, visible);
}
container.querySelectorAll('.rt-btn, .rt-title').forEach(el => {
el.style.color = colors.textColor;
});
container.querySelectorAll('.rt-icon-btn[data-accent="true"]').forEach(el => {
if (el.dataset.disabled === 'true') return;
const isActive = el.dataset.active === 'true';
el.style.color = isActive ? colors.navBtnColor : colors.textColor;
});
container.querySelectorAll('.rt-icon-btn[data-accent="false"]').forEach(el => {
el.style.color = colors.textColor;
});
container.querySelectorAll('[data-rt-color="progress-bg"]').forEach(el => {
el.style.background = isDarkColor(colors.controlBgColor)
? 'rgba(255,255,255,0.2)'
: 'rgba(0,0,0,0.1)';
});
}
function setIconBtnActive(overlay, action, active) {
if (!overlay) return;
const btn = overlay.querySelector(`#reader-immersive-bottom .rt-icon-btn[data-action="${action}"]`);
if (!btn || btn.dataset.disabled === 'true') return;
btn.dataset.active = active ? 'true' : 'false';
const isNight = overlay.__isNight === true;
const userBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor);
const colors = calculateColors(userBg, isNight);
btn.style.color = active ? colors.navBtnColor : colors.textColor;
}
function toggleImmersiveVisibility(overlay, show) {
const topEl = overlay.querySelector('#reader-immersive-top');
const botEl = overlay.querySelector('#reader-immersive-bottom');
if (!topEl || !botEl) return;
topEl.dataset.visible = show ? 'true' : 'false';
botEl.dataset.visible = show ? 'true' : 'false';
const isNight = overlay.__isNight === true;
const userBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor);
const colors = calculateColors(userBg, isNight);
applyImmersiveTopStyle(topEl, colors, topEl.dataset.style, show);
applyImmersiveBottomStyle(botEl, colors, botEl.dataset.style, show);
}
function createImmersiveTopBar(title, colors, onExit, onNewTab, style) {
const bar = document.createElement('div');
bar.id = 'reader-immersive-top';
bar.dataset.style = style;
bar.dataset.visible = 'false';
applyImmersiveTopStyle(bar, colors, style, false);
const back = document.createElement('div');
back.className = 'rt-btn';
back.style.cssText = `display: flex; align-items: center; gap: 4px; cursor: pointer;
padding: 6px 10px; border-radius: 6px; flex-shrink: 0;
color: ${colors.textColor}; -webkit-tap-highlight-color: transparent; outline: none;`;
back.innerHTML = ICONS.back + `返回`;
back.addEventListener('click', onExit);
const titleEl = document.createElement('div');
titleEl.className = 'rt-title';
titleEl.textContent = title || '';
titleEl.style.cssText = `flex: 1; text-align: center; font-size: 14px; font-weight: 500;
color: ${colors.textColor}; white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; margin: 0 8px; transition: color 0.3s ease;`;
const openBtn = document.createElement('div');
openBtn.className = 'rt-btn';
openBtn.style.cssText = `display: flex; align-items: center; gap: 4px; cursor: pointer;
padding: 6px 10px; border-radius: 6px; flex-shrink: 0;
color: ${colors.textColor}; -webkit-tap-highlight-color: transparent; outline: none;`;
openBtn.innerHTML = `打开` + ICONS.open;
openBtn.addEventListener('click', onNewTab);
bar.appendChild(back);
bar.appendChild(titleEl);
bar.appendChild(openBtn);
return bar;
}
function createImmersiveBottomBar(colors, onCatalogClick, onSettingsClick, onNightModeClick, getThemeMode, nav, style, getCurrentNav, onChapterNav, getAccentForClick) {
const bar = document.createElement('div');
bar.id = 'reader-immersive-bottom';
bar.dataset.style = style;
bar.dataset.visible = 'false';
applyImmersiveBottomStyle(bar, colors, style, false);
const row1 = document.createElement('div');
row1.className = 'rib-row1';
const prevBtn = document.createElement('div');
prevBtn.className = 'rt-btn';
prevBtn.id = 'reader-prev-btn';
prevBtn.textContent = '上一章';
prevBtn.style.cssText = `font-size: 13px; color: ${colors.textColor}; cursor: ${nav && nav.prev ? 'pointer' : 'not-allowed'}; opacity: ${nav && nav.prev ? 1 : 0.4}; padding: 4px 8px; transition: color 0.3s ease; -webkit-tap-highlight-color: transparent;`;
prevBtn.addEventListener('click', () => {
const currentNav = getCurrentNav ? getCurrentNav() : nav;
if (currentNav && currentNav.prev) {
if (onChapterNav) onChapterNav(currentNav.prev.href);
else window.location.href = currentNav.prev.href;
}
});
const progressContainer = document.createElement('div');
progressContainer.setAttribute('data-rt-color', 'progress-bg');
progressContainer.style.cssText = `flex: 1; height: 4px; background: ${isDarkColor(colors.controlBgColor) ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}; border-radius: 2px; margin: 0 12px; position: relative; cursor: default; opacity: 0.5; touch-action: none;`;
const progressThumb = document.createElement('div');
progressThumb.id = 'reader-progress-thumb';
progressThumb.style.cssText = `position: absolute; left: calc(0% - 7px); top: 50%; transform: translateY(-50%); width: 14px; height: 14px; background: ${colors.navBtnColor}; border-radius: 50%; box-shadow: 0 1px 4px rgba(0,0,0,0.3); transition: left 0.3s ease;`;
progressContainer.appendChild(progressThumb);
let isDraggingProgress = false;
const canDragProgress = () => catalogCache && catalogCache.links && catalogCache.links.length > 1;
const updateThumbByX = (clientX) => {
const rect = progressContainer.getBoundingClientRect();
if (!rect.width) return 0;
let percent = (clientX - rect.left) / rect.width;
percent = Math.max(0, Math.min(1, percent));
progressThumb.style.transition = 'none';
progressThumb.style.left = `calc(${percent * 100}% - 7px)`;
return percent;
};
const finishProgressDrag = (clientX) => {
progressThumb.style.transition = 'left 0.3s ease';
if (!canDragProgress()) return;
const percent = updateThumbByX(clientX);
const idx = Math.round(percent * (catalogCache.links.length - 1));
const target = catalogCache.links[idx];
if (!target || target.__missing || !target.href) {
if (target && target.__missing) notify('此章在原站缺失');
return;
}
if (onChapterNav) onChapterNav(target.href);
else window.location.href = target.href;
};
const onProgressTouchStart = (e) => {
if (!canDragProgress()) return;
if (!e.touches || !e.touches.length) return;
isDraggingProgress = true;
updateThumbByX(e.touches[0].clientX);
e.stopPropagation();
};
const onProgressTouchMove = (e) => {
if (!isDraggingProgress) return;
if (!e.touches || !e.touches.length) return;
updateThumbByX(e.touches[0].clientX);
e.stopPropagation();
};
const onProgressTouchEnd = (e) => {
if (!isDraggingProgress) return;
isDraggingProgress = false;
const t = (e.changedTouches && e.changedTouches[0]) ? e.changedTouches[0].clientX : null;
if (t !== null) finishProgressDrag(t);
};
const onProgressMouseDown = (e) => {
if (!canDragProgress()) return;
isDraggingProgress = true;
updateThumbByX(e.clientX);
e.stopPropagation();
};
const onProgressMouseMove = (e) => {
if (!isDraggingProgress) return;
updateThumbByX(e.clientX);
};
const onProgressMouseUp = (e) => {
if (!isDraggingProgress) return;
isDraggingProgress = false;
finishProgressDrag(e.clientX);
};
progressContainer.addEventListener('touchstart', onProgressTouchStart, { passive: true });
progressContainer.addEventListener('touchmove', onProgressTouchMove, { passive: true });
progressContainer.addEventListener('touchend', onProgressTouchEnd);
progressContainer.addEventListener('mousedown', onProgressMouseDown);
window.addEventListener('mousemove', onProgressMouseMove);
window.addEventListener('mouseup', onProgressMouseUp);
bar.__cleanup = () => {
window.removeEventListener('mousemove', onProgressMouseMove);
window.removeEventListener('mouseup', onProgressMouseUp);
};
const nextBtn = document.createElement('div');
nextBtn.className = 'rt-btn';
nextBtn.id = 'reader-next-btn';
nextBtn.textContent = '下一章';
nextBtn.style.cssText = `font-size: 13px; color: ${colors.textColor}; cursor: ${nav && nav.next ? 'pointer' : 'not-allowed'}; opacity: ${nav && nav.next ? 1 : 0.4}; padding: 4px 8px; transition: color 0.3s ease; -webkit-tap-highlight-color: transparent;`;
nextBtn.addEventListener('click', () => {
const currentNav = getCurrentNav ? getCurrentNav() : nav;
if (currentNav && currentNav.next) {
if (onChapterNav) onChapterNav(currentNav.next.href);
else window.location.href = currentNav.next.href;
}
});
row1.appendChild(prevBtn);
row1.appendChild(progressContainer);
row1.appendChild(nextBtn);
bar.appendChild(row1);
const row2 = document.createElement('div');
row2.style.cssText = 'display: flex; align-items: center; justify-content: space-around; padding: 4px 16px 10px;';
function makeIconBtn(iconSvg, label, onClick, disabled, action) {
const btn = document.createElement('div');
btn.className = 'rt-icon-btn';
btn.dataset.accent = 'true';
btn.dataset.action = action || '';
if (disabled) btn.dataset.disabled = 'true';
const baseColor = disabled ? colors.subtleTextColor : colors.textColor;
btn.style.cssText = `display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 2px; padding: 6px 12px; cursor: ${disabled ? 'not-allowed' : 'pointer'};
user-select: none; border-radius: 8px;
transition: color 0.25s ease, transform 0.15s ease, opacity 0.2s ease;
opacity: ${disabled ? 0.4 : 1};
color: ${baseColor}; -webkit-tap-highlight-color: transparent; outline: none;`;
btn.innerHTML = iconSvg + `${label}`;
if (!disabled) {
btn.addEventListener('click', () => {
const accent = getAccentForClick();
btn.style.color = accent;
btn.style.transform = 'scale(0.92)';
setTimeout(() => {
btn.style.transform = '';
const restoreColors = bar.__getRestoreColors ? bar.__getRestoreColors() : colors;
btn.style.color = (btn.dataset.active === 'true') ? restoreColors.navBtnColor : restoreColors.textColor;
}, 150);
onClick();
});
}
return btn;
}
row2.appendChild(makeIconBtn(
ICONS.catalog,
'目录',
() => {
const currentNav = getCurrentNav ? getCurrentNav() : nav;
if (currentNav && currentNav.index) onCatalogClick(currentNav.index.href);
},
!nav || !nav.index,
'catalog'
));
const themeBtn = document.createElement('div');
themeBtn.className = 'rt-icon-btn';
themeBtn.dataset.accent = 'false';
themeBtn.style.cssText = `display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 2px; padding: 6px 12px; cursor: pointer; user-select: none;
border-radius: 8px; transition: color 0.25s ease; color: ${colors.textColor};
-webkit-tap-highlight-color: transparent; outline: none;`;
function updateThemeBtn() {
const mode = getThemeMode();
let icon, label;
if (mode === 'night') { icon = ICONS.sun; label = '日间'; }
else { icon = ICONS.moon; label = '夜间'; }
themeBtn.innerHTML = icon + `${label}`;
}
updateThemeBtn();
themeBtn.addEventListener('click', () => {
onNightModeClick();
updateThemeBtn();
});
row2.appendChild(themeBtn);
row2.appendChild(makeIconBtn(ICONS.settings, '设置', onSettingsClick, false, 'settings'));
bar.appendChild(row2);
bar.__updateThemeBtn = updateThemeBtn;
return bar;
}
function refreshImmersiveNavButtons(overlay, nav) {
const prevBtn = overlay.querySelector('#reader-prev-btn');
const nextBtn = overlay.querySelector('#reader-next-btn');
if (prevBtn) {
prevBtn.style.cursor = nav && nav.prev ? 'pointer' : 'not-allowed';
prevBtn.style.opacity = nav && nav.prev ? '1' : '0.4';
}
if (nextBtn) {
nextBtn.style.cursor = nav && nav.next ? 'pointer' : 'not-allowed';
nextBtn.style.opacity = nav && nav.next ? '1' : '0.4';
}
}
function loadChapterInPlace(url, overlay) {
if (!url || !overlay) return;
const isNewTabMode = overlay.__addHashToNav === true;
const navUrl = isNewTabMode ? addReaderModeHash(url) : url;
const goFallback = () => {
notify('章节加载失败,跳转到新页面');
setTimeout(() => { window.location.href = navUrl; }, 800);
};
const cacheKey = normalizeUrl(url);
const cached = chapterCache.get(cacheKey);
if (cached && cached.article) {
chapterCache.delete(cacheKey);
try {
updateReaderContentFull(overlay, cached.article);
overlay.__currentUrl = url;
try { history.pushState({}, '', navUrl); } catch(e) {}
updateProgressBar(overlay, url);
schedulePreloadNext(overlay);
return;
} catch(e) {
// console.error('[阅读模式] 缓存渲染异常,回退到加载流程:', e);
}
}
if (currentPreloadTask && normalizeUrl(currentPreloadTask.url) === cacheKey) {
cancelCurrentPreload();
}
const loadingMask = document.createElement('div');
loadingMask.id = 'reader-chapter-loading';
loadingMask.style.cssText = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.3); z-index: 2147483000; display: flex; align-items: center; justify-content: center; pointer-events: auto; touch-action: none;`;
const loadingBox = document.createElement('div');
loadingBox.textContent = '正在加载...';
loadingBox.style.cssText = `background: rgba(0,0,0,0.75); color: #fff; padding: 12px 24px; border-radius: 8px; font-size: 14px;`;
loadingMask.appendChild(loadingBox);
overlay.appendChild(loadingMask);
const iframe = document.createElement('iframe');
iframe.style.cssText = 'position:absolute;width:0;height:0;border:none;visibility:hidden;';
iframe.src = url;
document.body.appendChild(iframe);
let loaded = false;
const timeout = setTimeout(() => {
if (!loaded) {
iframe.remove();
loadingMask.remove();
goFallback();
}
}, 15000);
iframe.onload = function() {
loaded = true;
clearTimeout(timeout);
const result = safeGetIframeDoc(iframe);
if (!result.doc) {
iframe.remove();
loadingMask.remove();
goFallback();
return;
}
let pollCount = 0;
const MAX_POLL_COUNT = 15;
const POLL_INTERVAL = 200;
const finishFail = (reason) => {
// console.error('[阅读模式] 动态加载失败:', reason || '正文轮询超时');
iframe.remove();
loadingMask.remove();
goFallback();
};
const tryExtract = () => {
let article = null;
try {
article = extractContent(result.doc);
} catch (e) {
// console.error('[阅读模式] 章节提取异常:', e);
}
if (article) {
try {
updateReaderContentFull(overlay, article);
overlay.__currentUrl = url;
try { history.pushState({}, '', navUrl); } catch(e) {}
updateProgressBar(overlay, url);
schedulePreloadNext(overlay);
} catch (e) {
finishFail('章节渲染异常: ' + e.message);
return;
}
iframe.remove();
loadingMask.remove();
return;
}
pollCount++;
if (pollCount < MAX_POLL_COUNT) {
setTimeout(tryExtract, POLL_INTERVAL);
} else {
finishFail('正文轮询超时');
}
};
tryExtract();
};
}
function updateReaderContentFull(overlay, article) {
if (!overlay || !article) return;
const titleEl = overlay.querySelector('#reader-article-title');
const contentSec = overlay.querySelector('#reader-content-section');
const bottomNav = overlay.querySelector('#reader-bottom-nav');
const styleEl = overlay.querySelector('#reader-content-style');
const immersiveTitle = overlay.querySelector('#reader-immersive-top .rt-title');
overlay.__nav = article.nav;
const isNight = overlay.__isNight === true;
const userBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor);
const colors = calculateColors(userBg, isNight);
if (titleEl) {
titleEl.textContent = article.title || '';
titleEl.style.color = colors.textColor;
titleEl.style.textAlign = 'left';
}
if (immersiveTitle) {
immersiveTitle.textContent = article.title || '';
}
if (contentSec) {
contentSec.innerHTML = article.content;
contentSec.style.color = colors.textColor;
}
if (styleEl) {
styleEl.textContent = buildContentStyleCSS(userBg, article.autoHasIndent, isNight);
}
const addHashToNav = overlay.__addHashToNav || false;
const onChapterNav = overlay.__onChapterNav || ((u) => { window.location.href = u; });
const onCatalogClick = (u) => showCatalog(u, overlay, onChapterNav);
const newBottom = createNavBar(article.nav, colors, addHashToNav, 'reader-bottom-nav', onCatalogClick, onChapterNav);
if (bottomNav && newBottom) bottomNav.replaceWith(newBottom);
else if (bottomNav && !newBottom) bottomNav.remove();
else if (!bottomNav && newBottom && contentSec && contentSec.parentNode) {
contentSec.parentNode.appendChild(newBottom);
}
refreshImmersiveNavButtons(overlay, article.nav);
overlay.scrollTo(0, 0);
}
function createShadowHost(hostId, bgColor) {
const host = document.createElement('div');
host.id = hostId;
host.style.cssText = 'all:initial;position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647;pointer-events:none;';
if (bgColor) host.style.background = bgColor;
const shadow = host.attachShadow({ mode: 'open' });
document.documentElement.appendChild(host);
return { host, shadow };
}
function buildReaderCore(article, initialUserBg, initialFontSize, onExit, overlayId, addHashToNav) {
let isNight = normalizeThemeMode(gmGet(STORAGE_KEYS.THEME_MODE, DEFAULT_SETTINGS.themeMode)) === 'night';
const initialActualBg = getEffectiveBgColor(initialUserBg, isNight);
const initialColors = calculateColors(initialUserBg, isNight);
const { host, shadow } = createShadowHost(overlayId + '-host', initialActualBg);
const baseStyle = document.createElement('style');
baseStyle.textContent = `
.rib-row1 {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 16px;
}
#reader-immersive-bottom.above-drawer .rib-row1 {
display: none;
}
.reader-locate-btn {
opacity: 0;
transform: scale(0.85) translateY(6px);
pointer-events: none;
transition:
opacity 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),
transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),
box-shadow 0.25s ease,
background 0.25s ease,
color 0.25s ease,
border-color 0.25s ease;
will-change: opacity, transform;
}
.reader-locate-btn.reader-locate-visible {
opacity: 1;
transform: scale(1) translateY(0);
pointer-events: auto;
}
.reader-locate-btn.reader-locate-visible:hover {
transform: scale(1.08) translateY(0);
box-shadow: 0 4px 14px rgba(0,0,0,0.24);
}
.reader-locate-btn.reader-locate-visible:active {
transform: scale(0.97) translateY(0);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
`;
shadow.appendChild(baseStyle);
['click', 'mousedown', 'mouseup', 'pointerdown', 'pointerup', 'touchstart', 'touchend', 'contextmenu', 'dblclick', 'auxclick'].forEach(evt => {
host.addEventListener(evt, (e) => { e.stopPropagation(); }, false);
});
const overlay = document.createElement('div');
overlay.id = overlayId;
overlay.__shadowHost = host;
overlay.__isNight = isNight;
overlay.__userBgColor = initialUserBg;
overlay.style.cssText = `position:absolute;top:0;left:0;width:100%;height:100%;background-color:${initialActualBg};overflow-y:auto;pointer-events:auto;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;-webkit-tap-highlight-color:transparent;outline:none;`;
const mainContainer = document.createElement('div');
mainContainer.id = 'reader-container';
mainContainer.style.cssText = `width:92%;max-width:800px;margin:0 auto;padding:20px 10px;color:${initialColors.textColor};font-size:${initialFontSize};line-height:1.8;user-select:text;overflow-wrap:break-word;word-wrap:break-word;text-align:left;`;
const contentArea = document.createElement('div');
contentArea.id = 'reader-content-area';
contentArea.style.cssText = 'text-align:left;';
let articleTitle = null;
let contentSection = null;
let bottomNavBar = null;
const style = document.createElement('style');
style.id = 'reader-content-style';
style.textContent = buildContentStyleCSS(initialUserBg, article.autoHasIndent, isNight);
mainContainer.appendChild(style);
overlay.__addHashToNav = addHashToNav;
overlay.__nav = article.nav;
overlay.__currentUrl = window.location.href;
const onChapterNav = (url) => {
loadChapterInPlace(url, overlay);
};
overlay.__onChapterNav = onChapterNav;
const onCatalogClick = (url) => { showCatalog(url, overlay, onChapterNav); };
const applyReaderColors = () => {
const nowIsNight = overlay.__isNight === true;
const nowUserBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor);
const nowActualBg = getEffectiveBgColor(nowUserBg, nowIsNight);
const colors = calculateColors(nowUserBg, nowIsNight);
overlay.style.backgroundColor = nowActualBg;
if (overlay.__shadowHost) overlay.__shadowHost.style.background = nowActualBg;
mainContainer.style.color = colors.textColor;
if (contentSection) contentSection.style.color = colors.textColor;
if (articleTitle) articleTitle.style.color = colors.textColor;
style.textContent = buildContentStyleCSS(nowUserBg, article.autoHasIndent, nowIsNight);
refreshImmersiveColors(overlay.querySelector('#reader-immersive-top'), colors, nowActualBg);
refreshImmersiveColors(overlay.querySelector('#reader-immersive-bottom'), colors, nowActualBg);
refreshCatalogColors(nowUserBg, nowIsNight);
};
const handleBgChange = (newColor) => {
if (overlay.__isNight) {
overlay.__isNight = false;
gmSet(STORAGE_KEYS.THEME_MODE, 'day');
const bottomBar = overlay.querySelector('#reader-immersive-bottom');
if (bottomBar && typeof bottomBar.__updateThemeBtn === 'function') bottomBar.__updateThemeBtn();
}
overlay.__userBgColor = newColor;
saveSetting(STORAGE_KEYS.BG_COLOR, newColor);
applyReaderColors();
};
const handleFontSizeChange = (newSize) => {
mainContainer.style.fontSize = newSize;
if (contentSection) contentSection.style.fontSize = newSize;
saveSetting(STORAGE_KEYS.FONT_SIZE, newSize);
};
const handleNightModeClick = () => {
overlay.__isNight = !overlay.__isNight;
gmSet(STORAGE_KEYS.THEME_MODE, overlay.__isNight ? 'night' : 'day');
applyReaderColors();
};
const getThemeMode = () => overlay.__isNight ? 'night' : 'day';
const getAccentForClick = () => {
const nowIsNight = overlay.__isNight === true;
const nowUserBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor);
return getAccentColor(nowUserBg, nowIsNight);
};
const getRestoreColors = () => {
const nowIsNight = overlay.__isNight === true;
const nowUserBg = overlay.__userBgColor || gmGet(STORAGE_KEYS.BG_COLOR, DEFAULT_SETTINGS.bgColor);
return calculateColors(nowUserBg, nowIsNight);
};
if (article.title && article.title.trim()) {
articleTitle = document.createElement('h1');
articleTitle.id = 'reader-article-title';
articleTitle.textContent = article.title;
articleTitle.style.cssText = `margin:0 0 30px 0;font-size:1.4em;font-weight:700;line-height:1.4;color:${initialColors.textColor};text-align:left;user-select:text;overflow-wrap:break-word;word-wrap:break-word;`;
}
contentSection = document.createElement('div');
contentSection.id = 'reader-content-section';
contentSection.innerHTML = article.content;
contentSection.style.cssText = `font-size:${initialFontSize};line-height:1.8;color:${initialColors.textColor};user-select:text;overflow-wrap:break-word;word-wrap:break-word;max-width:100%;text-align:left;`;
bottomNavBar = createNavBar(article.nav, initialColors, addHashToNav, 'reader-bottom-nav', onCatalogClick, onChapterNav);
if (articleTitle) contentArea.appendChild(articleTitle);
contentArea.appendChild(contentSection);
if (bottomNavBar) contentArea.appendChild(bottomNavBar);
mainContainer.appendChild(contentArea);
overlay.appendChild(mainContainer);
const immersiveTop = createImmersiveTopBar(article.title, initialColors, onExit, () => {
window.open(addReaderModeHash(originalUrl || window.location.href), '_blank');
}, 'edge');
const immersiveBottom = createImmersiveBottomBar(
initialColors,
onCatalogClick,
() => showSettingsPanel(overlay, {
onBgChange: handleBgChange,
onFontSizeChange: handleFontSizeChange
}),
handleNightModeClick,
getThemeMode,
article.nav,
'edge',
() => overlay.__nav,
onChapterNav,
getAccentForClick
);
immersiveBottom.__getRestoreColors = getRestoreColors;
overlay.appendChild(immersiveTop);
overlay.appendChild(immersiveBottom);
overlay.addEventListener('reader-catalog-ready', () => {
updateProgressBar(overlay);
});
updateProgressBar(overlay);
let immersiveVisible = false;
overlay.addEventListener('click', (e) => {
const t = e.target;
if (t && typeof t.closest === 'function' && t.closest(INTERACTIVE_SELECTOR)) return;
if (t.closest('#reader-immersive-top') ||
t.closest('#reader-immersive-bottom') ||
t.closest('#reader-top-nav') ||
t.closest('#reader-catalog-view') ||
t.closest('#reader-catalog-mask') ||
t.closest('#reader-settings-view') ||
t.closest('#reader-settings-mask') ||
t.closest('#reader-chapter-loading')) return;
immersiveVisible = !immersiveVisible;
toggleImmersiveVisibility(overlay, immersiveVisible);
});
overlay.addEventListener('scroll', () => {
if (immersiveVisible) {
immersiveVisible = false;
toggleImmersiveVisibility(overlay, false);
}
}, { passive: true });
shadow.appendChild(overlay);
overlay.focus();
if (article.nav && article.nav.index) {
setTimeout(() => {
preloadCatalog(article.nav.index.href, overlay);
}, 200);
}
if (article.nav && article.nav.next) {
schedulePreloadNext(overlay);
}
return overlay;
}
function buildOverlayReaderUI(article, initialBgColor = '#ffffff', initialFontSize = '18px') {
return buildReaderCore(article, initialBgColor, initialFontSize, exitLegacyReaderMode, 'reader-overlay', false);
}
function buildNewTabReaderUI(article, initialBgColor = '#ffffff', initialFontSize = '18px') {
const hiddenContainer = document.createElement('div');
hiddenContainer.id = 'original-page-hidden';
hiddenContainer.style.cssText = 'display:none;';
while (document.body.firstChild) hiddenContainer.appendChild(document.body.firstChild);
document.body.appendChild(hiddenContainer);
let overlay;
try {
overlay = buildReaderCore(article, initialBgColor, initialFontSize, () => window.close(), 'reader-newtab-overlay', true);
} catch(e) {
// console.error('[阅读模式] 新标签页构建失败,恢复原页面:', e);
while (hiddenContainer.firstChild) document.body.appendChild(hiddenContainer.firstChild);
hiddenContainer.remove();
throw e;
}
newTabOverlay = overlay;
return overlay;
}
function clearButtonTimer() {
if (buttonHideTimeout) { clearTimeout(buttonHideTimeout); buttonHideTimeout = null; }
}
function createMainButtonOnce() {
if (mainButton) return;
const btn = document.createElement('button');
btn.id = 'reader-main-btn';
btn.textContent = '阅读模式';
btn.style.cssText = 'position:fixed;bottom:60px;right:20px;z-index:10000;padding:8px 16px;background:#007bff;color:white;border:none;border-radius:4px;cursor:pointer;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;opacity:0.9;box-shadow:0 2px 8px rgba(0,0,0,0.2);transition:opacity 0.3s;user-select:none;-webkit-tap-highlight-color:transparent;outline:none;';
btn.addEventListener('mouseenter', () => {
if (isReaderMode) return;
btn.style.opacity = '1';
clearButtonTimer();
});
btn.addEventListener('mouseleave', () => {
if (isReaderMode) return;
btn.style.opacity = '0.9';
clearButtonTimer();
buttonHideTimeout = setTimeout(() => hideButton(btn), 3000);
});
btn.addEventListener('click', enterLegacyReaderMode);
document.body.appendChild(btn);
mainButton = btn;
buttonHideTimeout = setTimeout(() => hideButton(btn), 3000);
}
function hideButton(btn) {
if (!isReaderMode && btn && btn.parentNode) {
btn.style.opacity = '0';
setTimeout(() => {
if (btn.parentNode && !isReaderMode) btn.style.display = 'none';
}, 300);
}
}
function hideMainButtonPermanently() {
if (mainButton) { clearButtonTimer(); mainButton.style.display = 'none'; }
}
function enterLegacyReaderMode() {
if (isReaderMode) return;
hideMainButtonPermanently();
setTimeout(() => {
const article = extractContent();
if (!article) { notify('未找到可阅读的正文内容'); return; }
originalUrl = window.location.href.replace(/#.*$/, '');
scrollY = window.scrollY;
const body = document.body;
originalBodyOverflow = body.style.overflow;
originalBodyPosition = body.style.position;
originalBodyTop = body.style.top;
originalBodyWidth = body.style.width;
body.style.overflow = 'hidden';
body.style.position = 'fixed';
body.style.top = `-${scrollY}px`;
body.style.width = '100%';
const settings = loadSettings();
readerOverlay = buildOverlayReaderUI(article, settings.bgColor, settings.fontSize);
isReaderMode = true;
updateMenuCommands();
}, 50);
}
function exitLegacyReaderMode() {
if (!isReaderMode || !readerOverlay) return;
const immersiveBottom = readerOverlay.querySelector('#reader-immersive-bottom');
if (immersiveBottom && typeof immersiveBottom.__cleanup === 'function') {
immersiveBottom.__cleanup();
}
if (preloadScheduleTimer) {
clearTimeout(preloadScheduleTimer);
preloadScheduleTimer = null;
}
cancelCurrentPreload();
chapterCache.clear();
const target = readerOverlay.__shadowHost || readerOverlay;
target.remove();
readerOverlay = null;
currentCatalogView = null;
catalogCache = null;
catalogCacheLoading = null;
const body = document.body;
body.style.overflow = originalBodyOverflow;
body.style.position = originalBodyPosition;
body.style.top = originalBodyTop;
body.style.width = originalBodyWidth;
window.scrollTo(0, scrollY);
isReaderMode = false;
updateMenuCommands();
}
function activateNewTabReaderMode() {
const isMenuTriggered = new URLSearchParams(window.location.search).get('reader_trigger') === 'menu';
let attempts = 0;
const maxAttempts = 5;
const tryExtract = () => {
const article = extractContent();
if (article) {
originalUrl = window.location.href.replace(/#.*$/, '');
const settings = loadSettings();
try {
buildNewTabReaderUI(article, settings.bgColor, settings.fontSize);
} catch(e) {
notify('阅读模式初始化失败');
return;
}
updateMenuCommands();
} else {
attempts++;
if (attempts < maxAttempts) setTimeout(tryExtract, 300);
else if (isMenuTriggered) {
notify('未找到可阅读的正文内容,可能加载超时');
window.close();
}
}
};
tryExtract();
}
function openReaderInNewTab() {
window.open(addReaderModeHash(window.location.href), '_blank');
}
function updateMenuCommands() {
menuCommandIds.forEach(id => { try { GM_unregisterMenuCommand(id); } catch(e) {} });
menuCommandIds = [];
if (SHOULD_ACTIVATE_NEW_TAB) {
menuCommandIds.push(GM_registerMenuCommand('❌ 退出阅读模式', () => window.close()));
} else if (isReaderMode) {
menuCommandIds.push(GM_registerMenuCommand('❌ 退出阅读模式', exitLegacyReaderMode));
} else {
menuCommandIds.push(GM_registerMenuCommand('打开阅读模式', enterLegacyReaderMode));
menuCommandIds.push(GM_registerMenuCommand('新标签页打开阅读模式', openReaderInNewTab));
}
}
function init() {
if (SHOULD_ACTIVATE_NEW_TAB) {
activateNewTabReaderMode();
} else {
let retryCount = 0;
const maxRetries = 5;
const intervalId = setInterval(() => {
const testArticle = extractContent();
retryCount++;
if (testArticle || retryCount >= maxRetries) {
clearInterval(intervalId);
if (testArticle) createMainButtonOnce();
updateMenuCommands();
}
}, 300);
}
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();