🏠 返回首頁 

Greasy Fork is available in English.

番茄小说阅读助手

自动滚动页面 + 快捷键翻页

// ==UserScript==
// @name         番茄小说阅读助手
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  自动滚动页面 + 快捷键翻页
// @author       return null;
// @match        https://fanqienovel.com/reader/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=fanqienovel.com
// @grant        none
// @license      MIT
// ==/UserScript==
(function () {
'use strict';
const utils = {
/**
* toast,默认 1.5s 后关闭,若传入 duration,则按照 duration 的值关闭,若 duration 为 0,则不关闭,返回一个对象,其中有一个 close 方法,可以手动关闭
* @param {*} msg
* @param {*} duration
*/
toast: (msg, duration = 1500) => {
// 优先把之前的 toast 关闭
const lastToast = document.querySelector('.fanqie-zhushou-toast');
if (lastToast) {
lastToast.remove();
}
const toast = document.createElement('div');
toast.className = 'fanqie-zhushou-toast';
toast.innerHTML = msg;
document.body.appendChild(toast);
if (duration) {
setTimeout(() => {
toast.remove();
}, duration);
}
return {
close: () => {
toast.remove();
}
}
},
initConfig: () => {
const defaultConfig = {
version: '20230730002',
/**
* 阅读器宽度,支持百分比和 px
*/
width: '80%',
/**
* 快捷键
*/
hotKeys: {
/**
* 上一章快捷键
*/
lastChapter: 'ArrowLeft',
/**
* 下一章快捷键
*/
nextChapter: 'ArrowRight',
/**
* 关闭自动滚动快捷键
*/
closeAutoScroll: 'Escape'
},
/**
* 自动滚动速度,单位毫秒
*/
autoScrollSpeed: 50
}
// 优先从 localStorage 中获取配置,没有就用默认配置,判断版本号是否一致,不一致就用默认配置
const config = JSON.parse(localStorage.getItem('fanqie-zhushou-config')) || defaultConfig;
if (config.version !== defaultConfig.version) {
localStorage.setItem('fanqie-zhushou-config', JSON.stringify(defaultConfig));
return defaultConfig;
}
localStorage.setItem('fanqie-zhushou-config', JSON.stringify(config));
return config;
},
refreshConfig: (config) => {
localStorage.setItem('fanqie-zhushou-config', JSON.stringify(config));
},
addToolbarBtn: ({
title,
svg,
onclick
}) => {
const toolbar = document.querySelector('#app .reader-toolbar > div')
const autoScrollBtn = document.createElement('div');
autoScrollBtn.className = 'reader-toolbar-item';
autoScrollBtn.title = title;
autoScrollBtn.innerHTML = `
${svg || ''}
<div>${title}</div>
`
if (onclick) {
autoScrollBtn.onclick = onclick
}
toolbar.appendChild(autoScrollBtn);
}
}
// 优先从 localStorage 中获取配置,没有就用默认配置
const config = utils.initConfig()
const titleNavWidth = '300px'
const style = document.createElement('style');
const pageWidthStyle = `
#app div.muye-reader div.muye-reader-inner {
width: calc(${config.width} - ${titleNavWidth});
max-width: calc(${config.width} - ${titleNavWidth});
}
.muye-reader-nav {
width: calc(${config.width} - 15px - ${titleNavWidth});
max-width: calc(${config.width} - 15px - ${titleNavWidth});
}
`;
style.innerHTML = `
${config.width ? pageWidthStyle : ''}
.reader-toolbar {
left: 85%;
}
.reader-toolbar > div > div {
cursor: pointer;
}
.reader-toolbar-item.reader-toolbar-item-download {
display: none;
}
.fanqie-zhushou-toast {
position: fixed;
top: 35px;
left: 50%;
transform: translate(-50%, -50%);
padding: 10px 20px;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
border-radius: 5px;
z-index: 999;
}
.line-space {
height: 52px;
width: 100%;
}
.auto-header-title {
position: absolute;
top: 50%;
transform: translate(0, -50%);
font-size: 16px;
width: 295px;
left: 5px;
font-weight: bold;
}
.auto-header-title h1 {
font-size: unset;
font-weight: unset;
margin: unset;
padding-bottom: 5px;
}
`;
document.head.appendChild(style);
const lastChapter = () => {
const btn = document.querySelector('#app .chapter-btn.last');
if (btn) {
btn.click();
}
}
const nextChapter = () => {
const btn = document.querySelector('#app .chapter-btn.next');
if (btn) {
btn.click();
}
}
const autoScroll = () => {
const autoScrollBtn = document.querySelector('#app .reader-toolbar-item[title="滚动"]');
if (autoScrollBtn) {
autoScrollBtn.setAttribute('status', 'on');
clearInterval(window.autoScrollTimer);
window.autoScrollTimer = setInterval(() => {
const reader = document.querySelector('#app .muye-reader');
reader.scrollBy(0, 1)
// 根据页面高度计算进度,保留两位小数
const progress = (reader.scrollTop / (reader.scrollHeight - reader.offsetHeight) * 100).toFixed(1);
utils.toast(`已开启自动滚动,按 Esc 可退出,当前进度:${progress}%,当前速度:${config.autoScrollSpeed}`, 0);
}, config.autoScrollSpeed);
}
}
// 监听键盘方向键
document.addEventListener('keydown', (e) => {
console.log('keydown', e);
if (e.key === config.hotKeys.lastChapter) {
lastChapter();
} else if (e.key === config.hotKeys.nextChapter) {
nextChapter();
}
const autoScrollBtn = document.querySelector('#app .reader-toolbar-item[title="滚动"]');
if (autoScrollBtn) {
// esc 主动关闭自动滚动
if (e.key === config.hotKeys.closeAutoScroll) {
const autoScrollBtn = document.querySelector('#app .reader-toolbar-item[title="滚动"]');
autoScrollBtn.setAttribute('status', 'off');
clearInterval(window.autoScrollTimer);
utils.toast('已关闭自动滚动');
}
// 如果当前在自动滚动时,按下 + 或者 - 可以调整滚动速度
if (e.key === '+' || e.key === '=') {
const status = autoScrollBtn.getAttribute('status');
if (status === 'on') {
config.autoScrollSpeed -= 5;
autoScroll()
utils.refreshConfig(config);
}
} else if (e.key === '-') {
const status = autoScrollBtn.getAttribute('status');
if (status === 'on') {
config.autoScrollSpeed += 5;
autoScroll()
utils.refreshConfig(config);
}
}
}
});
utils.addToolbarBtn({
title: '滚动',
svg: `
<svg t="1690709388609" class="muyeicon-icon muyeicon-icon-scan reader-toolbar-item-icon" viewBox="0 0 #### ####" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2017" width="200" height="200">
<path d="M256.2 736.4h320c88.2 0 160-71.8 160-160v-320c0-88.2-71.8-160-160-160h-320c-88.2 0-160 71.8-160 160v320c0 88.2 71.8 160 160 160z m-96-480c0-52.9 43.1-96 96-96h320c52.9 0 96 43.1 96 96v320c0 52.9-43.1 96-96 96h-320c-52.9 0-96-43.1-96-96v-320zM768.2 815.6H521.5c-12.3-28.3-40.5-48-73.3-48s-61 19.8-73.3 48H128.2c-17.7 0-32 14.3-32 32s14.3 32 32 32h246.7c12.3 28.2 40.5 48 73.3 48s61-19.7 73.3-48h246.7c17.7 0 32-14.3 32-32s-14.3-32-32-32zM879.8 375.1V128.4c0-17.7-14.3-32-32-32s-32 14.3-32 32v246.7c-28.2 12.3-48 40.5-48 73.3s19.7 61 48 73.3v246.7c0 17.7 14.3 32 32 32s32-14.3 32-32V521.7c28.3-12.3 48-40.5 48-73.3s-19.8-61-48-73.3z" p-id="2018">
</path>
</svg>
`,
onclick: (event) => {
const autoScrollBtn = document.querySelector('#app .reader-toolbar-item[title="滚动"]');
const status = autoScrollBtn.getAttribute('status');
if (status === 'on') {
autoScrollBtn.setAttribute('status', 'off');
clearInterval(window.autoScrollTimer);
utils.toast('已关闭自动滚动');
} else {
autoScroll()
}
}
})
const analysisChapterData = (html) => {
if (!html || html.indexOf('window.__INITIAL_STATE__=') === -1) {
return null;
}
const startIndex = html.indexOf('window.__INITIAL_STATE__=')
const endIndex = html.indexOf('</script>', startIndex)
let jsonStr = html.substring(startIndex + 25, endIndex - 1)
// 找到结尾的 ;
const lastSemicolonIndex = jsonStr.lastIndexOf(';')
jsonStr = jsonStr.substring(0, lastSemicolonIndex)
const data = JSON.parse(jsonStr)
console.log('analysisChapterData', data)
const r###lt = data.reader.chapterData;
const contentHtmlStartIndex = html.indexOf('<div class="muye-reader-box-header">');
const contentHtmlEndIndex = html.indexOf('<div class="muye-reader-btns">', contentHtmlStartIndex);
const $content_html = html.substring(contentHtmlStartIndex, contentHtmlEndIndex);
r###lt.$content_html = $content_html
return r###lt
};
window.$fna = {
next_item_id: null,
item_loading: false,
item_content_caches: {}
}
window.onload = () => {
window.$fna.next_item_id = window.__INITIAL_STATE__.reader.chapterData.nextItemId;
window.$fna.item_loading = false
console.log('window.$fna.next_item_id', window.$fna.next_item_id)
}
const preloadNextChapter = async ({ itemId, skipCache }) => {
if (window.$fna.item_content_caches[itemId] && skipCache !== true) {
return window.$fna.item_content_caches[itemId]
}
const response = await fetch(`https://fanqienovel.com/reader/${itemId}?enter_from=reader`, {
"method": "GET",
});
const html = await response.text()
const chapterData = analysisChapterData(html);
window.$fna.item_loading = false
window.$fna.item_content_caches[itemId] = chapterData
return chapterData
}
const loadNextChapter = async ({ itemId, skipCache }) => {
if (window.$fna.item_loading === true) {
return
}
if (window.$fna.item_content_caches[itemId] && skipCache !== true) {
return window.$fna.item_content_caches[itemId]
}
window.$fna.item_loading = true
const chapterData = await preloadNextChapter({ itemId, skipCache })
preloadNextChapter({ itemId: chapterData.nextItemId, skipCache: true })
window.$fna.item_loading = false
return chapterData
}
const reader = document.querySelector('#app .muye-reader');
reader.addEventListener('scroll', (event) => {
console.log('scroll', event);
document.querySelector('.muye-reader-btns').style.display = 'none';
const scrollTop = event.target.scrollTop;
const scrollHeight = event.target.scrollHeight;
const offsetHeight = event.target.offsetHeight;
const progress = (scrollTop / (scrollHeight - offsetHeight) * 100).toFixed(4);
console.log('progress', progress)
if (progress >= 90) {
loadNextChapter({ itemId: window.$fna.next_item_id })
.then(({ $content_html, nextItemId }) => {
const readerBoxElement = document.querySelector('#app .muye-reader .muye-reader-inner .muye-reader-box');
readerBoxElement.innerHTML = readerBoxElement.innerHTML + '<div class="line-space"></div>' + $content_html;
window.$fna.next_item_id = nextItemId;
const titles = document.querySelectorAll('h1.muye-reader-title')
// 获取所有标题,赋值到要给字符串数组中
const titleArr = []
titles.forEach((title) => {
titleArr.push(`<h1>${title.innerText}</h1>`)
})
// 在 #app div 中插入一个 auto-header-title 的 div,用于存放标题
// 如果存在就先删掉
let autoHeaderTitleElement = document.querySelector('#app div.auto-header-title')
autoHeaderTitleElement && autoHeaderTitleElement.remove()
autoHeaderTitleElement = document.createElement('div')
autoHeaderTitleElement.className = 'auto-header-title'
autoHeaderTitleElement.innerHTML = titleArr.join('')
document.querySelector('#app div').appendChild(autoHeaderTitleElement)
document.querySelectorAll('#app div.auto-header-title h1').forEach((item, index) => {
item.onclick = () => {
console.log('click', index)
// 滚动到对应的标题,稍微往上偏移一点,平滑滚动
const titles = document.querySelectorAll('h1.muye-reader-title')
titles[index].scrollIntoView({
behavior: 'smooth'
})
}
})
})
}
})
})();