// ==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);
}
})();