Greasy Fork is available in English.

Funimation Utilities

Add useful functionalities to Funimation. Keyboard skipping, next episode buttons, and more.

// ==UserScript==
// @name         Funimation Utilities
// @namespace    https://theusaf.github.io
// @version      1.1
// @description  Add useful functionalities to Funimation. Keyboard skipping, next episode buttons, and more.
// @author       theusaf
// @match        https://funimation.com/shows/*/*
// @match        https://www.funimation.com/shows/*/*
// @grant        none
// ==/UserScript==

/* global TITLE_DATA */

// Adds an episode list
// Note: may need rework to prevent issue where images dont show up
(function(){
  if(location.pathname.split("/").length < 5){
    return;
  }
  // data for the program initialization
  const path = location.pathname,
    [,,showSlug] = path.split("/"),
    {titleSlug,seriesId,episodeNum,seasonNum} = TITLE_DATA,
    apiRequest = new XMLHttpRequest();

  // getting all episodes
  apiRequest.open("GET",`https://${location.host}/api/episodes/?title_id=${seriesId}&season=${seasonNum}&sort=order&sort_direction=ASC`);
  apiRequest.send();
  apiRequest.onload = function(){
    // genearting the html
    const sectionAfter = document.querySelector(".video-information"),// document.querySelector("#comments-section"),
      section = sectionAfter.parentNode.insertBefore(document.createElement("section"),sectionAfter),
      data = JSON.parse(apiRequest.responseText);
    section.id = "episodes-section";
    section.innerHTML = `<style>
      #gallery-container{
        display: flex;
        border-top: 1px solid #ccc;
        padding-top: 0.5rem;
      }
      #episode-gallery{
        flex: 1;
        display: flex;
      }
      .episode-thumb{
        height: 7rem;
      }
      .episode-item{
        position: relative;
        width: 12.5rem;
      }
      .gallery-control{
        margin: 0.5rem;
        font-size: 2rem;
        font-weight: bold;
        text-align: center;
        align-self: center;
      }
      .episode-num{
        display: block;
        font-weight: bold;
      }
      .episode-preview{
        position: absolute;
        background: black;
        border-radius: 0.25rem;
        top: 100%;
        color: white;
        padding: 0.5rem;
        z-index: 1;
      }
      .episode-preview > span{
        font-weight: bold;
      }
    </style>
    <div id="gallery-container">
        <div id="previous-gallery" class="gallery-control">&lt;</div>
        <div id="episode-gallery"></div>
        <div id="next-gallery" class="gallery-control">&gt;</div>
    </div>`;
    const [prev,episodes,next] = document.querySelector("#gallery-container").children,
      {items} = data;
    function IsVisible(elem){
      const box = elem.getBoundingClientRect(),
        {top,bottom,right,left} = box,
        winh = window.innerHeight,
        winw = window.innerWidth,
        topEdgeInRange = top >= 0 && top <= winh,
        bottomEdgeInRange = bottom >= 0 && bottom <= winh,
        leftEdgeInRange = left >= 0 && left <= winw,
        rightEdgeInRange = right >= 0 && right <= winw,
        coverScreenHorizontally = left <= 0 && right >= winw,
        coverScreenVertically = top <= 0 && bottom >= winh,
        topEdgeInScreen = topEdgeInRange && (leftEdgeInRange || rightEdgeInRange || coverScreenHorizontally),
        bottomEdgeInScreen = bottomEdgeInRange && (leftEdgeInRange || rightEdgeInRange || coverScreenHorizontally),
        leftEdgeInScreen = leftEdgeInRange && (topEdgeInRange || bottomEdgeInRange || coverScreenVertically),
        rightEdgeInScreen = rightEdgeInRange && (topEdgeInRange || bottomEdgeInRange || coverScreenVertically);
      return topEdgeInScreen && bottomEdgeInScreen && leftEdgeInScreen && rightEdgeInScreen;
    }
    let firstindex = +episodeNum - 3;
    if(firstindex < 0){
      firstindex = 0;
    }
    let index = 0;
    for(let episode of items){
      const {episodeNum,episodeSlug,runtime,episodeName,titleSlug} = episode.item,
        {synopsis,thumb} = episode;
      // generate items
      const container = document.createElement("div"),
        preview = document.createElement("div");
      container.className = "episode-item";
      container.innerHTML = `<a href="/shows/${titleSlug}/${episodeSlug}/${location.search}">
        <img class="episode-thumb" src="${thumb}" alt="${episodeNum}: ${episodeName}">
        <div class="episode-meta">
          <span class="episode-num">Episode ${episodeNum}</span>
          <span class="episode-name">${episodeName}</span>
        </div>
      </a>`;
      preview.className = "episode-preview";
      preview.innerHTML = `<span>${episodeName}</span>
      <p>${synopsis}</p>`;
      episodes.append(container);
      if(index < firstindex){
        container.style.display = "none";
        index++;
      }
      let wait;
      container.addEventListener("mouseenter",()=>{
        wait = setTimeout(()=>{
          container.append(preview);
        },750);
      });
      container.addEventListener("mouseleave",()=>{
        clearTimeout(wait);
        try{preview.remove();}catch(e){}
      });
    }
    function check(){
      const items = document.querySelectorAll(".episode-item");
      for(let i = 0;i<items.length;i++){
        if(!IsVisible(items[i])){
          items[i].style.display = "none";
        }
      }
      if(!episodes.querySelector(".episode-item:not([style])")){
        for(let i = 0;i<items.length;i++){
          items[i].removeAttribute("style");
          if(i < firstindex){
            items[i].style.display = "none";
          }
        }
        setTimeout(check,500);
      }
    }
    setTimeout(check,500);
    prev.addEventListener("click",()=>{
      const visible = Array.from(episodes.querySelectorAll(".episode-item:not([style])")),
        all = Array.from(episodes.children);
      if(visible[0] === all[0]){
        return;
      }
      const index = all.indexOf(visible[0]);
      let targetIndex = index - visible.length;
      visible.reverse();
      if(targetIndex < 0){
        targetIndex = 0;
      }
      for(let i = targetIndex,j=0;i<targetIndex + index;i++,j++){
        all[i].removeAttribute("style");
        visible[j].style.display = "none";
      }
    });
    next.addEventListener("click",()=>{
      const visible = Array.from(episodes.querySelectorAll(".episode-item:not([style])")),
        all = Array.from(episodes.children);
      if(visible[visible.length - 1] === all[all.length - 1]){
        return;
      }
      const index = all.indexOf(visible[visible.length - 1]);
      let targetIndex = index + visible.length;
      if(targetIndex >= all.length){
        targetIndex = all.length - 1;
      }
      for(let i = 0;i<targetIndex - index;i++){
        visible[i].style.display = "none";
        all[index + i + 1].removeAttribute("style");
      }
    });
  };
})();

// Adds keyboard functions.
function addKeyboardFunctions(){
  if(location.pathname.split("/").length < 5){
    return;
  }
  const playerframe = document.querySelector("#player"),
    {contentDocument,contentWindow} = playerframe,
    player = contentDocument.querySelector("#brightcove-player"),
    video = contentDocument.querySelector("#brightcove-player_html5_api");
  if(video === null){
    return setTimeout(addKeyboardFunctions,2e3);
  }
  const {fp} = contentWindow;
  contentWindow.addEventListener("keydown",(event)=>{
    let activated = true;
    switch(event.code){
      case "KeyL":{
        fp.player.currentTime(fp.player.currentTime() + 10);
        break;
      }
      case "ArrowRight":{
        fp.player.currentTime(fp.player.currentTime() + 5);
        break;
      }
      case "KeyJ":{
        fp.player.currentTime(fp.player.currentTime() - 10);
        break;
      }
      case "ArrowLeft":{
        fp.player.currentTime(fp.player.currentTime() - 5);
        break;
      }
      case "KeyK":{
        if(fp.player.paused()){
          fp.player.play();
        }else{
          fp.player.pause();
        }
        break;
      }
      default:{
        activated = false;
      }
    }
    if(/Digit\d/.test(event.code)){
      activated = true;
      const int = +event.code.match(/\d/)[0],
        time = (int / 10) * video.duration;
      fp.player.currentTime(time);
    }
    if(activated){
      event.preventDefault();
    }
  });
};
addKeyboardFunctions();