// ==UserScript== // @name 小窗净读器(通用适配/目录分页/多页拼合/可拖可调/记忆进度/可扩展) // @namespace https://github.com/jx-j-x/Greasemonkey-script // @version 0.6.2 // @description Alt+L 输入链接→抽正文;Alt+T 目录(每页50条,可跳页);Alt+R 续读上次;←/→ 翻页/跳章;↑/↓ 平滑滚动;Ctrl+Alt+X 显示/隐藏。多站点适配可扩展,跨域抓取(含 GBK/Big5),小窗可拖动/调整大小,自动记忆目录与最后章节链接。 // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect * // @connect 3wwd.com // @connect m.3wwd.com // @connect biquge.tw // @connect www.biquge.tw // @connect m.biquge.tw // @run-at document-idle // ==/UserScript== (function () { 'use strict'; // ========== 样式 ========== GM_addStyle(` #cr-panel{ position: fixed; left: 16px; bottom: 16px; width: 300px; height: 300px; background: #fff; color:#222; border:1px solid #ddd; border-radius:10px; box-shadow:0 6px 24px rgba(0,0,0,.15); z-index: 2147483646; display:none; overflow:hidden; font:14px/1.7 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,"Noto Sans","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif; } #cr-drag{ position:absolute; top:0; left:0; right:0; height:10px; cursor:move; z-index:3; } #cr-resize{ position:absolute; right:2px; bottom:2px; width:14px; height:14px; cursor:nwse-resize; z-index:3; opacity:.7; background:linear-gradient(135deg, rgba(0,0,0,0) 0, rgba(0,0,0,0) 50%, #cfcfcf 50%, #cfcfcf 100%); border-radius:3px; } #cr-content{ height:100%; overflow:auto; padding:12px 14px; position:relative; z-index:1; } #cr-content p{ margin:0 0 6px 0; } /* URL 输入弹窗 & 目录弹窗 */ #cr-modal, #cr-toc{ position: fixed; inset: 0; background: rgba(0,0,0,.35); display:none; align-items: center; justify-content: center; z-index: 2147483647; } #cr-modal .cr-box, #cr-toc .cr-box{ width: min(800px, 96vw); background:#fff; border-radius:12px; box-shadow: 0 10px 30px rgba(0,0,0,.25); padding: 16px; } #cr-modal h3, #cr-toc h3{ margin:0 0 10px 0; font-size:16px; } #cr-url{ width:100%; box-sizing:border-box; padding:10px 12px; font-size:14px; border:1px solid #ddd; border-radius:8px; outline:none; } #cr-modal .ops{ margin-top:12px; display:flex; gap:8px; justify-content:flex-end; } #cr-modal button{ border:1px solid #ddd; background:#fff; border-radius:8px; padding:6px 12px; cursor:pointer; } #cr-modal button.primary{ background:#111; color:#fff; border-color:#111; } /* 目录弹窗 */ #cr-toc .cr-box{ padding:12px 12px 8px;} .cr-toc-head{ display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; } .cr-toc-head .title{ font-weight:600; } .cr-toc-head .close{ border:1px solid #ddd; background:#fff; border-radius:8px; padding:6px 10px; cursor:pointer; } .toc-list{ max-height: 65vh; overflow:auto; border:1px solid #eee; border-radius:8px; padding: 6px; } .toc-item{ padding:6px 8px; border-radius:6px; cursor:pointer; user-select:none; display:flex; gap:10px; align-items:flex-start; } .toc-item:hover{ background:#f6f6f6; } .toc-item.active{ background:#111; color:#fff; } .toc-idx{ min-width: 56px; opacity:.7; font-variant-numeric: tabular-nums; } .toc-title{ flex:1; word-break: break-all; } .cr-toc-foot{ display:flex; align-items:center; justify-content:space-between; margin-top:8px; gap:12px; flex-wrap:wrap; } .range{ font-size:12px; color:#666; } .pager{ display:flex; align-items:center; gap:8px; } .pager button{ border:1px solid #ddd; background:#fff; border-radius:8px; padding:6px 10px; cursor:pointer; } .pager input{ width:90px; padding:6px 8px; border:1px solid #ddd; border-radius:8px; outline:none; font-size:14px; } `); // ========== DOM ========== const panel = document.createElement('div'); panel.id = 'cr-panel'; panel.innerHTML = `
'); return txt ? `
${txt}
` : '(未找到正文容器)
'; } function cleanContentNode(node, baseUrl) { const n = node.cloneNode(true); try { n.querySelectorAll('script,style,ins,.adsbygoogle,.ad,[class*="ad-"],.advert,[id^="hm_t_"],.recommend,.toolbar').forEach(e=>e.remove()); } catch {} n.querySelectorAll('img').forEach(img => { const src = img.getAttribute('src') || ''; try { img.src = new URL(src, baseUrl).href; } catch { img.src = src; } img.style.maxWidth = '100%'; }); n.querySelectorAll('a').forEach(a => { const href = safeHref(a.getAttribute('href') || ''); if (!href) { a.removeAttribute('href'); return; } try { a.href = new URL(href, baseUrl).href; } catch { a.href = href; } a.rel = 'noreferrer noopener'; }); return n.innerHTML || '(正文为空)
'; } // ========== 存储 ========== const LS_KEY_PANEL = 'cr_reader_panel_state'; const LS_KEY_PROGRESS = 'cr_reader_progress'; function savePanelState() { const rect = panel.getBoundingClientRect(); const data = { x: rect.left, y: rect.top, w: rect.width, h: rect.height }; try { localStorage.setItem(LS_KEY_PANEL, JSON.stringify(data)); } catch {} } function restorePanelState() { try { const raw = localStorage.getItem(LS_KEY_PANEL); if (!raw) return; const { x, y, w, h } = JSON.parse(raw); if (Number.isFinite(x) && Number.isFinite(y)) { panel.style.top = Math.max(2, Math.min(y, window.innerHeight - 50)) + 'px'; panel.style.left = Math.max(2, Math.min(x, window.innerWidth - 50)) + 'px'; panel.style.bottom = ''; panel.style.right = ''; } if (Number.isFinite(w) && Number.isFinite(h)) { const cw = Math.max(220, Math.min(w, window.innerWidth - 10)); const ch = Math.max(160, Math.min(h, window.innerHeight - 10)); panel.style.width = cw + 'px'; panel.style.height = ch + 'px'; } } catch {} } function clampIntoViewport() { const rect = panel.getBoundingClientRect(); let x = rect.left, y = rect.top, w = rect.width, h = rect.height; const maxX = window.innerWidth - w - 2; const maxY = window.innerHeight - h - 2; x = Math.max(2, Math.min(x, Math.max(2, maxX))); y = Math.max(2, Math.min(y, Math.max(2, maxY))); panel.style.left = x + 'px'; panel.style.top = y + 'px'; } window.addEventListener('resize', () => { clampIntoViewport(); savePanelState(); }); function saveProgress({ tocUrl, chapterUrl, seriesId }) { const bookBase = (state.profile?.deriveBookBase?.(tocUrl || chapterUrl)) || generic.deriveBookBase(tocUrl || chapterUrl) || ''; const payload = { tocUrl: tocUrl || null, chapterUrl: chapterUrl || null, seriesId: seriesId || null, bookBase, updatedAt: Date.now() }; try { localStorage.setItem(LS_KEY_PROGRESS, JSON.stringify(payload)); } catch {} } function getSavedProgress() { try { const raw = localStorage.getItem(LS_KEY_PROGRESS); if (!raw) return null; const o = JSON.parse(raw); if (!o || (!o.tocUrl && !o.chapterUrl)) return null; return o; } catch { return null; } } // ========== 抓取 ========== function decodeText(arrayBuffer, headersStr) { const lower = (headersStr || '').toLowerCase(); const m = lower.match(/charset\s*=\s*([^\s;]+)/); const fromHeader = m && m[1] ? m[1].replace(/["']/g,'').toLowerCase() : ''; const tryDec = enc => { try { return new TextDecoder(enc).decode(arrayBuffer); } catch { return null; } }; let text = null; if (/big5/.test(fromHeader)) text = tryDec('big5') || tryDec('utf-8'); else if (/gbk|gb18030|gb2312/.test(fromHeader)) text = tryDec('gbk') || tryDec('gb18030') || tryDec('utf-8'); else text = tryDec('utf-8') || tryDec('gbk') || tryDec('gb18030') || tryDec('big5'); if (!text) text = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)); const hint = (text.match(/]+charset\s*=\s*["']?\s*([a-z0-9-]+)/i) || [])[1]; if (hint) { const h = hint.toLowerCase(); if (/big5/.test(h)) text = tryDec('big5') || text; else if (/gb/.test(h)) text = tryDec('gbk') || tryDec('gb18030') || text; } return text; } function gmFetch(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, responseType: 'arraybuffer', headers: { 'Accept': 'text/html,application/xhtml+xml' }, timeout: 30000, onload: (res) => { try { const html = decodeText(res.response, res.responseHeaders || ''); resolve({ html, finalUrl: url, headers: res.responseHeaders || '' }); } catch (e) { reject(e); } }, onerror: reject, ontimeout: () => reject(new Error('请求超时')), }); }); } const parseHTML = html => new DOMParser().parseFromString(html, 'text/html'); // ========== 正文抽取(走 profile,可回落通用) ========== function extractMain(doc, baseUrl) { console.log('profile:', state.profile); if (state.profile?.extractContent) { try { return state.profile.extractContent(doc, baseUrl); } catch {} } return preferFirst(doc, [ '#content', '#chaptercontent', '#chapterContent', '.content', '.read-content', '#contentTxt', '#BookText', '#txtContent' ], baseUrl); } // ========== 上下页 & 目录链接 ========== function getNavUrls(doc, baseUrl) { if (state.profile?.findNav) { try { return state.profile.findNav(doc, baseUrl); } catch {} } return generic.findNav(doc, baseUrl); } function getInfoUrl(doc, baseUrl, entryUrl) { if (state.profile?.findInfoUrl) { try { return state.profile.findInfoUrl(doc, baseUrl, entryUrl); } catch {} } return generic.findInfoUrl(doc, baseUrl, entryUrl); } // ========== 抓取章节(多页拼合) ========== async function fetchChapterSeries(entryUrl) { const visited = new Set(); state.loading = true; state.pages = []; state.pageIndex = 0; state.seriesId = getSeriesIdFromUrl(entryUrl); state.nextChapterUrl = null; state.prevChapterUrl = null; state.profile = chooseProfile(entryUrl); const newBase = state.profile.deriveBookBase?.(entryUrl) || generic.deriveBookBase(entryUrl) || null; if (state.bookBase && newBase && state.bookBase !== newBase) { state.tocItems = []; // 切书:清空老目录缓存 } state.bookBase = newBase; renderInfo('正在抓取章节…'); try { const first = await gmFetch(entryUrl); const firstDoc = parseHTML(first.html); state.tocUrl = getInfoUrl(firstDoc, entryUrl, entryUrl); saveProgress({ tocUrl: state.tocUrl, chapterUrl: entryUrl, seriesId: state.seriesId }); const { prev: prev0, next: next0 } = getNavUrls(firstDoc, entryUrl); state.prevChapterUrl = (prev0 && !isSameChapterPage(prev0, entryUrl)) ? prev0 : null; state.pages.push({ url: entryUrl, html: extractMain(firstDoc, entryUrl) }); visited.add(new URL(entryUrl, location.href).href); // 连抓分页 let cursor = next0, step = 0; while (cursor && isSameChapterPage(cursor, entryUrl) && step < 50) { const abs = new URL(cursor, entryUrl).href; if (visited.has(abs)) break; visited.add(abs); const pg = await gmFetch(abs); const d = parseHTML(pg.html); state.pages.push({ url: abs, html: extractMain(d, abs) }); const nav = getNavUrls(d, abs); cursor = nav.next; step++; await sleep(60); } if (cursor && !isSameChapterPage(cursor, entryUrl)) state.nextChapterUrl = cursor; if (!state.pages.length) throw new Error('未抓到正文'); showCurrentPage(); } catch (err) { console.error('[clean-reader] 抓取失败:', err); renderInfo('抓取失败:' + (err && err.message ? err.message : '未知错误')); } finally { state.loading = false; } } // ========== 目录抓取与渲染 ========== async function openTOC() { state.modalOpen = true; // 切书校验:目录缓存属于别的书则清空 const saved = getSavedProgress?.() || null; const desiredBase = (state.tocUrl ? (state.profile?.deriveBookBase?.(state.tocUrl) || generic.deriveBookBase(state.tocUrl)) : (saved?.tocUrl ? (state.profile?.deriveBookBase?.(saved.tocUrl) || generic.deriveBookBase(saved.tocUrl)) : generic.deriveBookBase(location.href))) || null; const currBase = state.tocItems.length ? generic.deriveBookBase(state.tocItems[0].href) : null; if (currBase && desiredBase && currBase !== desiredBase) state.tocItems = []; toc.style.display = 'flex'; $('#cr-toc-goto').value = ''; if (!state.tocUrl) { const sp = getSavedProgress(); if (sp && sp.tocUrl) state.tocUrl = sp.tocUrl; else state.tocUrl = state.bookBase || generic.deriveBookBase(location.href); } if (!state.tocItems.length) { tocListEl.innerHTML = `