// ==UserScript== // @name FIMFiction - Remaining Words and Reading Time // @namespace Selbi // @include http*://fimfiction.net/* // @include http*://www.fimfiction.net/* // @version 3.0 // @description Displays average reading time left and overall story progress. // @downloadURL none // ==/UserScript== ////////////////////////////////////// // Read Time in Words-Per-Minute const WPM = 267; // You must enter your own speed! ////////////////////////////////////// (function() { var storyContainers = document.querySelectorAll("article.story_container"); for (story of storyContainers) { parseStory(story); } function parseStory(story) { // Global variables var readWordsNode = document.createElement("b"); var outOfTextNode = document.createElement("span"); var totalWordCountElem = story.querySelector(".chapters-footer > .word_count > b"); var remainingTimeNode = document.createElement("span"); var progressBarProgressNode = document.createElement("div"); var totalWordCount = parseIntFull(totalWordCountElem.innerHTML); var totalReadWords = 0; var readChapters = 0; var totalChapters = 0; // Reusable hook (with timeout troubleshooting) var 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(parseChapters(true)); // "x of y words" box outOfTextNode.innerHTML = " of "; totalWordCountElem.before(outOfTextNode); outOfTextNode.before(readWordsNode); // Write total remaining reading time Object.assign(remainingTimeNode.style, {fontSize: "90%", opacity: "0.8", marginRight: "1em", fontSpacing: "1px"}); writeReadTime(); readWordsNode.before(remainingTimeNode); // Create and insert the progress bar var progressBarNode = document.createElement("div"); progressBarNode.style.height = "4px"; var barWidth = getPercent(totalReadWords, totalWordCount); progressBarProgressNode.title = barWidth; Object.assign(progressBarProgressNode.style, { width: barWidth, backgroundColor: "green", height: "inherit", borderBottomLeftRadius: "4px", borderBottomRightRadius: "4px", transition: "width 0.2s ease-out" }); 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(isInit) { // All chapters minus the "Show" button for long stories var chapterElems = story.querySelectorAll(".chapters > li > div:not(.chapter_expander)"); totalChapters = chapterElems.length; // Reset accus var readWords = 0; readChapters = 0; for (ch of chapterElems) { // Element references var readIconElem = ch.querySelector("a.chapter-read-icon"); var wordCountElem = ch.querySelector("div.word_count span.word-count-number"); // Total word count var chapterWordCount = parseIntFull(wordCountElem.innerHTML); if (readIconElem.classList.contains("chapter-read")) { readWords += chapterWordCount; readChapters++; } // Gets called only once (wrapped into the same function to reduce loading times) if (isInit) { // Reading time var readTimeNode = document.createElement("span"); readTimeNode.innerHTML = convertToTime(chapterWordCount); Object.assign(readTimeNode.style, {fontSize: "80%", opacity: "0.5", marginRight: "1em"}); wordCountElem.before(readTimeNode); wordCountElem.parentNode.title = getPercent(chapterWordCount, totalWordCount); // Hook readIconElem.addEventListener("click", updateHandler, false); } } totalReadWords = readWords; return readWords; } // Gets called on page load and on every function updateRemainingReadTime() { readWordsNode.innerHTML = numberWithCommas(parseChapters(false)); writeReadTime(); var 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"; if (totalReadWords > 0 && readChapters < totalChapters) { readWordsNode.classList.remove("hidden"); outOfTextNode.classList.remove("hidden"); remainingTimeNode.innerHTML = convertToTime(totalReadWords) + " of " + convertToTime(totalWordCount); return; } readWordsNode.classList.add("hidden"); outOfTextNode.classList.add("hidden"); remainingTimeNode.innerHTML = convertToTime(totalWordCount); } } /////////////////// // Formatting functions function parseIntFull(number) { return parseInt(number.replace(",", "").trim()); } function numberWithCommas(number) { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } function convertToTime(wordCount) { var 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.round(num1 / num2 * 10000) / 100).toFixed(2) + "%"; } })();