FIMFiction - Remaining Words and Reading Time

Displays average reading time left and overall story progress.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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