// ==UserScript== // @name 连续下一页 // @namespace https://scriptcat.org/zh-CN/script-show-page/5556 // @version 1.1.1 // @description 智能预加载下一页,滚动到底部自动拼接,无感加载,不影响网速。 // @author YZD // @license MIT // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_notification // @grant unsafeWindow // @connect * // @run-at document-end // @supportURL https://scriptcat.org/zh-CN/users/192915 // @homepageURL https://scriptcat.org/zh-CN/users/192915 // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjY3ZWVhIiBzdHJva2Utd2lkdGg9IjIiPjxwYXRoIGQ9Ik0xMiA1djE0bTAgMGw0LTQtNC00bS00IDRsNC00LTQtNCIvPjwvc3ZnPg== // ==/UserScript== (function() { 'use strict'; const CONFIG = { preloadDistance: 800, cooldown: 1000, maxPages: 10, debug: false, storageKey: 'autopager_state' }; const userConfig = { preloadDistance: GM_getValue('preloadDistance', CONFIG.preloadDistance), cooldown: GM_getValue('cooldown', CONFIG.cooldown), maxPages: GM_getValue('maxPages', CONFIG.maxPages), enabled: GM_getValue('enabled', true) }; const logger = { log: (...args) => CONFIG.debug && console.log('[AutoPager]', ...args), warn: (...args) => console.warn('[AutoPager]', ...args), error: (...args) => console.error('[AutoPager]', ...args) }; const STYLES = { loader: ` position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%) translateY(100px); background: rgba(0,0,0,0.85); color: #fff; padding: 12px 24px; border-radius: 24px; font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; z-index: 2147483647; opacity: 0; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 4px 12px rgba(0,0,0,0.15); pointer-events: none; backdrop-filter: blur(8px); `, loaderActive: ` opacity: 1; transform: translateX(-50%) translateY(0); `, separator: ` text-align: center; padding: 30px 20px; color: #888; font-size: 13px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; border-top: 1px solid #e0e0e0; margin: 30px 0; position: relative; `, contentWrapper: ` animation: autopagerFadeIn 0.4s ease-out; `, keyframes: ` @keyframes autopagerFadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } ` }; const SITE_RULES = [ { name: 'generic', priority: 0, test: () => true, nextLink: () => { const selectors = [ 'a[rel="next"]', '.pagination .next a', '.page-next', '.next-page', '#nextPage', 'a[title="下一页"]', 'a[title="Next"]', 'a:has-text("下一页")', 'a:has-text("Next")', 'a:has-text("»")', 'a:has-text("›")' ]; for (let sel of selectors) { if (sel.includes(':has-text')) { const text = sel.match(/:has-text\("(.+)"\)/)[1]; const links = Array.from(document.querySelectorAll('a')); const found = links.find(a => a.textContent.trim().includes(text)); if (found) return found; } else { const el = document.querySelector(sel); if (el) return el; } } return null; }, contentSelector: () => { const selectors = [ 'article', '.content', '.main', '#content', '.post-list', '.article-list', '.thread-list', 'main', '[role="main"]', '#main' ]; for (let sel of selectors) { const el = document.querySelector(sel); if (el) return el; } return document.body; }, filterContent: (element) => { const removeSelectors = [ 'script', 'nav', 'header', 'footer', 'aside', '.ads', '.advertisement', '#header', '#footer', '.sidebar' ]; removeSelectors.forEach(sel => { element.querySelectorAll(sel).forEach(el => el.remove()); }); return element; } } ]; class AutoPager { constructor() { if (!userConfig.enabled) { logger.log('AutoPager 已禁用'); return; } this.state = { loading: false, lastLoadTime: 0, loadedPages: 0, history: [location.href], isComplete: false }; this.elements = {}; this.observer = null; this.init(); } init() { this.injectStyles(); this.createElements(); this.bindEvents(); this.setupMenu(); setTimeout(() => this.checkScroll(), 500); logger.log('AutoPager 初始化完成'); } injectStyles() { const style = document.createElement('style'); style.textContent = STYLES.keyframes; document.head.appendChild(style); } createElements() { this.elements.loader = document.createElement('div'); this.elements.loader.className = 'autopager-loader'; this.elements.loader.style.cssText = STYLES.loader; this.elements.loader.innerHTML = ` 加载下一页... `; document.body.appendChild(this.elements.loader); this.elements.endMarker = document.createElement('div'); this.elements.endMarker.className = 'autopager-end'; this.elements.endMarker.style.cssText = STYLES.separator; this.elements.endMarker.innerHTML = '—— 没有更多内容了 ——'; this.elements.endMarker.style.display = 'none'; } bindEvents() { const sentinel = document.createElement('div'); sentinel.style.cssText = 'position: absolute; bottom: 0; left: 0; width: 1px; height: 1px; pointer-events: none;'; document.body.appendChild(sentinel); this.observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting && !this.state.loading && !this.state.isComplete) { this.checkScroll(); } }); }, { rootMargin: `0px 0px ${userConfig.preloadDistance}px 0px` }); this.observer.observe(sentinel); document.addEventListener('click', (e) => { const link = e.target.closest('a'); if (link && this.isNextPageLink(link)) { e.preventDefault(); e.stopPropagation(); this.loadNextPage(link.href); } }, true); } setupMenu() { if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand('🔄 重置自动加载', () => { this.reset(); GM_notification({ title: 'AutoPager', text: '已重置加载状态', timeout: 2000 }); }); GM_registerMenuCommand(userConfig.enabled ? '⏸️ 暂停自动加载' : '▶️ 启用自动加载', () => { GM_setValue('enabled', !userConfig.enabled); location.reload(); }); } } isNextPageLink(link) { if (!link || !link.href) return false; const text = link.textContent.trim(); const rel = link.getAttribute('rel'); const cls = link.className || ''; const id = link.id || ''; return rel === 'next' || text.includes('下一页') || text.includes('Next') || text === '»' || text === '›' || cls.includes('next') || id === 'nextPage'; } checkScroll() { if (this.state.loading || this.state.isComplete || this.state.loadedPages >= userConfig.maxPages) { return; } const nextLink = this.getNextLink(); if (nextLink && !this.isLoaded(nextLink.href)) { this.loadNextPage(nextLink.href); } else if (!nextLink) { this.markComplete(); } } getNextLink() { for (let rule of SITE_RULES) { if (rule.test()) { return rule.nextLink(); } } return null; } isLoaded(url) { return this.state.history.includes(url); } loadNextPage(url) { const now = Date.now(); if (now - this.state.lastLoadTime < userConfig.cooldown) { setTimeout(() => this.checkScroll(), userConfig.cooldown); return; } this.state.loading = true; this.state.lastLoadTime = now; this.showLoader(true); logger.log('正在加载:', url); GM_xmlhttpRequest({ method: 'GET', url: url, headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'X-Requested-With': 'XMLHttpRequest' }, timeout: 15000, onload: (response) => { if (response.status === 200) { this.handleSuccess(response.responseText, url); } else { this.handleError(new Error(`HTTP ${response.status}`)); } }, onerror: (error) => { this.handleError(error); }, ontimeout: () => { this.handleError(new Error('请求超时')); } }); } handleSuccess(html, url) { try { const doc = new DOMParser().parseFromString(html, 'text/html'); const content = this.extractContent(doc); if (content) { this.appendContent(content, url); this.updateNextLink(doc); this.state.loadedPages++; this.state.history.push(url); this.saveState(); this.prefetchNext(doc); logger.log(`第 ${this.state.loadedPages} 页加载完成`); } else { this.markComplete(); } } catch (err) { logger.error('解析内容失败:', err); } finally { this.state.loading = false; this.showLoader(false); } } handleError(error) { logger.error('加载失败:', error); this.state.loading = false; this.showLoader(false); if (typeof GM_notification !== 'undefined') { GM_notification({ title: 'AutoPager', text: '加载失败,请手动点击下一页', timeout: 3000 }); } } extractContent(doc) { const rule = SITE_RULES.find(r => r.test()); if (!rule) return null; let container = rule.contentSelector(); if (!container) return null; const cloned = container.cloneNode(true); const filtered = rule.filterContent ? rule.filterContent(cloned) : cloned; return filtered.innerHTML; } appendContent(html, url) { const rule = SITE_RULES.find(r => r.test()); const container = rule ? rule.contentSelector() : document.body; const separator = document.createElement('div'); separator.className = 'autopager-separator'; separator.style.cssText = STYLES.separator; separator.innerHTML = `第 ${this.state.loadedPages + 2} 页`; separator.dataset.pageUrl = url; const wrapper = document.createElement('div'); wrapper.className = 'autopager-content'; wrapper.style.cssText = STYLES.contentWrapper; wrapper.innerHTML = html; this.sanitizeContent(wrapper); container.appendChild(separator); container.appendChild(wrapper); this.dispatchEvent('contentLoaded', { url, page: this.state.loadedPages + 2 }); } sanitizeContent(element) { element.querySelectorAll('script').forEach(el => el.remove()); element.querySelectorAll('*').forEach(el => { const attrs = el.attributes; for (let i = attrs.length - 1; i >= 0; i--) { const name = attrs[i].name; if (name.startsWith('on')) { el.removeAttribute(name); } } }); } updateNextLink(doc) { const newLink = this.getNextLinkFromDoc(doc); const currentLink = this.getNextLink(); if (currentLink && newLink) { currentLink.href = newLink.href; currentLink.onclick = (e) => { e.preventDefault(); this.loadNextPage(newLink.href); }; } else if (currentLink && !newLink) { this.markComplete(); } } getNextLinkFromDoc(doc) { const originalDoc = document; unsafeWindow.document = doc; let link = null; for (let rule of SITE_RULES) { if (rule.test()) { link = rule.nextLink(); break; } } unsafeWindow.document = originalDoc; return link; } prefetchNext(doc) { const nextLink = this.getNextLinkFromDoc(doc); if (nextLink && !this.isLoaded(nextLink.href)) { const prefetchLink = document.createElement('link'); prefetchLink.rel = 'prefetch'; prefetchLink.href = nextLink.href; document.head.appendChild(prefetchLink); logger.log('预加载:', nextLink.href); } } markComplete() { this.state.isComplete = true; if (this.elements.endMarker.parentNode) return; const rule = SITE_RULES.find(r => r.test()); const container = rule ? rule.contentSelector() : document.body; container.appendChild(this.elements.endMarker); this.elements.endMarker.style.display = 'block'; if (this.observer) { this.observer.disconnect(); } } showLoader(show) { const loader = this.elements.loader; if (show) { loader.style.cssText = STYLES.loader + STYLES.loaderActive; } else { loader.style.cssText = STYLES.loader; } } saveState() { const state = { history: this.state.history, loadedPages: this.state.loadedPages, timestamp: Date.now() }; GM_setValue(CONFIG.storageKey + '_' + location.hostname, JSON.stringify(state)); } loadState() { try { const saved = GM_getValue(CONFIG.storageKey + '_' + location.hostname); if (saved) { const state = JSON.parse(saved); if (Date.now() - state.timestamp < 3600000) { return state; } } } catch (e) { logger.error('读取状态失败:', e); } return null; } reset() { this.state.loadedPages = 0; this.state.history = [location.href]; this.state.isComplete = false; GM_setValue(CONFIG.storageKey + '_' + location.hostname, ''); location.reload(); } dispatchEvent(name, detail) { const event = new CustomEvent('autopager:' + name, { detail }); document.dispatchEvent(event); } destroy() { if (this.observer) this.observer.disconnect(); if (this.elements.loader) this.elements.loader.remove(); if (this.elements.endMarker) this.elements.endMarker.remove(); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } function init() { if (window.self !== window.top) return; setTimeout(() => { window.autoPager = new AutoPager(); }, 1000); } })();