// ==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"><</div>
<div id="episode-gallery"></div>
<div id="next-gallery" class="gallery-control">></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();