您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays average reading time left and overall story progress.
// ==UserScript== // @name FIMFiction - Remaining Words and Reading Time // @namespace Selbi // @include http*://fimfiction.net/* // @include http*://www.fimfiction.net/* // @version 3.2 // @description Displays average reading time left and overall story progress. // ==/UserScript== ////////////////////////////////////// // Read Time in Words-Per-Minute const WPM = 220; // You must enter your own speed! ////////////////////////////////////// (function() { // Set up CSS let style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = ` #remainingTimeNode { font-size: 90%; opacity: 0.8; margin-right: 1em; } #progressBarProgressNode { background-color: green; height: inherit; border-bottom-left-radius: 4px; border-bottom-right-radius: 4px; transition: width 0.2s ease-out; } .readTime { font-size: 80%; opacity: 0.5; margin-right: 1em; } @media (max-width: 1280px) { .story_container .chapters-footer .word_count { position: initial; margin-top: 6px; } .story_container .chapters-footer { padding-right: 10px; } } `; document.querySelector("head").appendChild(style); // Parse on page load let storyContainers = document.querySelectorAll("article.story_container"); for (story of storyContainers) { parseStory(story); } function parseStory(story) { // Global variables let readWordsNode = document.createElement("b"); let outOfTextNode = document.createElement("span"); let totalWordCountElem = story.querySelector(".chapters-footer > .word_count > b"); let remainingTimeNode = document.createElement("span"); remainingTimeNode.id = "remainingTimeNode"; let progressBarProgressNode = document.createElement("div"); progressBarProgressNode.id = "progressBarProgressNode"; let totalWordCount = parseIntFull(totalWordCountElem.innerHTML); let totalReadWords = 0; let readChapters = 0; let totalChapters = 0; // Reusable hook (with timeout troubleshooting) let updateHandler = function(){ setTimeout(function(){ updateRemainingReadTime(); }, 1000); }; // One-time call at page loag (function init() { // Add hook for toggle all chapters button story.querySelector(".chapters-footer > a").addEventListener("click", updateHandler, false); // Parse chapters for the first time readWordsNode.innerHTML = numberWithCommas(totalWordCount - parseChapters()); // "x of y words" box outOfTextNode.innerHTML = " of "; totalWordCountElem.before(outOfTextNode); outOfTextNode.before(readWordsNode); // Write total remaining reading time writeReadTime(); readWordsNode.before(remainingTimeNode); // Create and insert the progress bar let progressBarNode = document.createElement("div"); progressBarNode.style.height = "4px"; let barWidth = getPercent(totalReadWords, totalWordCount); progressBarProgressNode.title = barWidth; progressBarProgressNode.style.width = barWidth; progressBarNode.appendChild(progressBarProgressNode); story.querySelector(".chapters-footer").after(progressBarNode); })(); // Central function to read the word count and reading status of each chapter // Also adds reading times for each chapter on page loag function parseChapters() { // All chapters minus the "Show" button for long stories let chapterElems = story.querySelectorAll(".chapters > li > div:not(.chapter_expander)"); totalChapters = chapterElems.length; // Reset accus let readWords = 0; readChapters = 0; for (let ch of chapterElems) { // Element references let readIconElem = ch.querySelector("a.chapter-read-icon"); let wordCountElem = ch.querySelector("div.word_count span.word-count-number"); // Skip unpublished chapters if (readIconElem.parentNode.querySelector("img") != null) { totalChapters--; continue; } // Total word count let chapterWordCount = parseIntFull(wordCountElem.innerHTML); // Check if chapter is read let isRead = readIconElem.classList.contains("chapter-read"); // Increase global read progress if (isRead) { readWords += chapterWordCount; readChapters++; } // Check if this is an in-progress chapter add its partial read percentage if available let readProgress = ch.parentElement.querySelector(".read-progress"); let partialReadWordsForChapter = 0; if (readProgress != null) { let inProgressReadPercentage = parseFloat(readProgress.style.width) / 100.0; partialReadWordsForChapter = Math.round(chapterWordCount * inProgressReadPercentage); if (!isRead) { readWords += partialReadWordsForChapter; } } // Reading time let readTimeNode = wordCountElem.parentNode.querySelector(".readTime"); if (readTimeNode == null) { // Create new readTimeNode = document.createElement("span"); readTimeNode.classList = "readTime"; wordCountElem.before(readTimeNode); wordCountElem.parentNode.title = getPercent(chapterWordCount, totalWordCount); // Hook readIconElem.addEventListener("click", updateHandler, false); } let readTimeText = convertToTime(chapterWordCount); if (partialReadWordsForChapter > 0) { readTimeText = convertToTime(chapterWordCount - partialReadWordsForChapter) + " (of " + convertToTime(chapterWordCount) + ")"; } readTimeNode.innerHTML = readTimeText; } if (readChapters >= totalChapters) { totalReadWords = totalWordCount; } else { totalReadWords = readWords; } return readWords; } // Gets called on page load and on every function updateRemainingReadTime() { readWordsNode.innerHTML = numberWithCommas(parseChapters()); writeReadTime(); let percent = getPercent(totalReadWords, totalWordCount); progressBarProgressNode.style.width = percent; progressBarProgressNode.title = percent; } // Read time with respect to the fact whether a story is read or not function writeReadTime() { remainingTimeNode.title = readChapters + " / " + totalChapters + " chapters read (" + convertToTime(totalReadWords) + ")"; if (totalReadWords > 0 && readChapters < totalChapters) { readWordsNode.classList.remove("hidden"); outOfTextNode.classList.remove("hidden"); remainingTimeNode.innerHTML = convertToTime(totalWordCount - totalReadWords) + " of " + convertToTime(totalWordCount) + " remaining"; return; } readWordsNode.classList.add("hidden"); outOfTextNode.classList.add("hidden"); remainingTimeNode.innerHTML = convertToTime(totalWordCount); } } /////////////////// // Formatting functions function parseIntFull(number) { return parseInt(number.replace(/,/g, "").trim()); } function numberWithCommas(number) { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } function convertToTime(wordCount) { let time = (Math.ceil(wordCount / WPM)); if (time > 60) { time = ((Math.ceil(time / 6)) / 10).toFixed(1) + " h"; } else { time += " min"; } return time; } function getPercent(num1, num2) { return Math.min(100, (Math.round(num1 / num2 * 10000) / 100)).toFixed(2) + "%"; } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址