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