Skip to content

Instantly share code, notes, and snippets.

@Ellivers
Last active March 30, 2025 08:03
AnimePahe Improvements
// ==UserScript==
// @name AnimePahe Improvements
// @namespace https://gist.github.com/Ellivers/f7716b6b6895802058c367963f3a2c51
// @downloadURL https://gist.github.com/Ellivers/f7716b6b6895802058c367963f3a2c51/raw/anime-tracker.user.js
// @match https://animepahe.com/*
// @match https://animepahe.org/*
// @match https://animepahe.ru/*
// @match https://kwik.*/e/*
// @match https://kwik.*/f/*
// @grant GM_getValue
// @grant GM_setValue
// @version 4.1.0
// @author Ellivers
// @license MIT
// @description Improvements and additions for the AnimePahe site
// ==/UserScript==
/*
How to install:
* Get the Violentmonkey browser extension (Tampermonkey is largely untested, but seems to work as well).
* For the GitHub Gist page, click the "Raw" button on this page.
* For Greasy Fork, click "Install this script".
* I highly suggest using an ad blocker (uBlock Origin is recommended)
Feature list:
* Automatically redirects to the correct session when a tab with an old session is loaded. No more having to search for the anime and find the episode again!
* Saves your watch progress of each video, so you can resume right where you left off.
* Bookmark anime and view it in a bookmark menu.
* Add ongoing anime to an episode feed to easily check when new episodes are out.
* Quickly visit the download page for a video, instead of having to wait 5 seconds when clicking the download link.
* Find collections of anime series in the search results, with the series listed in release order.
* Jump directly to the next anime's first episode from the previous anime's last episode, and the other way around.
* Hide all episode thumbnails on the site, for those who are extra wary of spoilers (and for other reasons).
* Saved data can be viewed and deleted in the "Manage Data" menu.
* Reworked anime index page. You can now:
* Find anime with your desired genre, theme, type, demographic, status and season.
* Search among these filter results.
* Open a random anime within the specified filters.
* Automatically finds a relevant cover for the top of anime pages.
* Adds points in the video player progress bar for opening, ending, and other highlights (only available for some anime).
* Adds a button to skip openings and endings when they start (only available for some anime).
* Frame-by-frame controls on videos, using ',' and '.'
* Skip 10 seconds on videos at a time, using 'J' and 'L'
* Changes the video 'loop' keybind to Shift + L
* Press Shift + N to go to the next episode, and Shift + P to go to the previous one.
* Speed up or slow down a video by holding Ctrl and:
* Scrolling up/down
* Pressing the up/down keys
* You can also hold shift to make the speed change more gradual.
* Remembers the selected speed for each anime.
* Enables you to see images from the video while hovering over the progress bar.
* Allows you to also use numpad number keys to seek through videos.
* Theatre mode for a better non-fullscreen video experience on larger screens.
* Instantly loads the video instead of having to click a button to load it.
* Adds an "Auto-Play Video" option to automatically play the video (on some browsers, you may need to allow auto-playing for this to work).
* Adds an "Auto-Play Next" option to automatically go to the next episode when the current one is finished.
* Focuses on the video player when loading the page, so you don't have to click on it to use keyboard controls.
* Adds an option to automatically choose the highest quality available when loading the video.
* Adds a button (in the settings menu) to reset the video player.
* Shows the dates of when episodes were added.
* And more!
*/
const baseUrl = window.location.toString();
const initialStorage = getStorage();
function getDefaultData() {
return {
version: 2,
linkList:[],
videoTimes:[],
bookmarks:[],
notifications: {
lastUpdated: Date.now(),
anime: [],
episodes: []
},
badCovers: [],
settings: {
autoDelete:true,
hideThumbnails:false,
theatreMode:false,
bestQuality:true,
autoDownload:true,
autoPlayNext:false,
autoPlayVideo:false,
seekThumbnails:true,
seekPoints:true,
skipButton:true,
reduceMotion:false
},
videoSpeed: []
};
}
function upgradeData(data, fromver) {
if (fromver === undefined) {
fromver = 0;
}
const defaultVer = getDefaultData().version;
if (fromver >= defaultVer) return;
console.log(`[AnimePahe Improvements] Upgrading data from version ${fromver}`);
/* Changes:
* V1:
* autoPlay -> autoPlayNext
* v2:
* autoDelete -> settings.autoDelete
* hideThumbnails -> settings.hideThumbnails
* theatreMode -> settings.theatreMode
* bestQuality -> settings.bestQuality
* autoDownload -> settings.autoDownload
* autoPlayNext -> settings.autoPlayNext
* autoPlayVideo -> settings.autoPlayVideo
* +videoSpeed
*/
const upgradeFunctions = [
() => { // for V0
data.autoPlayNext = data.autoPlay;
delete data.autoPlay;
},
() => { // for V1
const settings = {};
settings.autoDelete = data.autoDelete;
settings.hideThumbnails = data.hideThumbnails;
settings.theatreMode = data.theatreMode;
settings.bestQuality = data.bestQuality;
settings.autoDownload = data.autoDownload;
settings.autoPlayNext = data.autoPlayNext;
settings.autoPlayVideo = data.autoPlayVideo;
data.settings = settings;
delete data.autoDelete;
delete data.hideThumbnails;
delete data.theatreMode;
delete data.bestQuality;
delete data.autoDownload;
delete data.autoPlayNext;
delete data.autoPlayVideo;
}
]
for (let i = fromver; i < defaultVer; i++) {
const fn = upgradeFunctions[i];
if (fn !== undefined) fn();
}
data.version = defaultVer;
}
function getStorage() {
const defa = getDefaultData();
const res = GM_getValue('anime-link-tracker', defa);
const oldVersion = res.version;
for (const key of Object.keys(defa)) {
if (res[key] !== undefined) continue;
res[key] = defa[key];
}
for (const key of Object.keys(defa.settings)) {
if (res.settings[key] !== undefined) continue;
res.settings[key] = defa.settings[key];
}
if (oldVersion !== defa.version) {
upgradeData(res, oldVersion);
saveData(res);
}
return res;
}
function saveData(data) {
GM_setValue('anime-link-tracker', data);
}
function secondsToHMS(secs) {
const mins = Math.floor(secs/60);
const hrs = Math.floor(mins/60);
const newSecs = Math.floor(secs % 60);
return `${hrs > 0 ? hrs + ':' : ''}${mins % 60}:${newSecs.toString().length > 1 ? '' : '0'}${newSecs % 60}`;
}
function getStoredTime(name, ep, storage, id = undefined) {
if (id !== undefined) {
return storage.videoTimes.find(a => a.episodeNum === ep && a.animeId === id);
}
else return storage.videoTimes.find(a => a.animeName === name && a.episodeNum === ep);
}
function applyCssSheet(cssString) {
$("head").append('<style id="anitracker-style" type="text/css"></style>');
const sheet = $("#anitracker-style")[0].sheet;
const rules = cssString.split(/^\}/mg).map(a => a.replace(/\n/gm,'') + '}');
for (let i = 0; i < rules.length - 1; i++) {
sheet.insertRule(rules[i], i);
}
}
const kwikDLPageRegex = /^https:\/\/kwik\.\w+\/f\//;
// Video player improvements
if (/^https:\/\/kwik\.\w+/.test(baseUrl)) {
if (typeof $ !== "undefined" && $() !== null) anitrackerKwikLoad(window.location.origin + window.location.pathname);
else {
const scriptElem = document.querySelector('head > link:nth-child(12)');
if (scriptElem == null) {
const h1 = document.querySelector('h1');
// Some bug that the kwik DL page had before
// (You're not actually blocked when this happens)
if (!kwikDLPageRegex.test(baseUrl) && h1.textContent == "Sorry, you have been blocked") {
h1.textContent = "Oops, page failed to load.";
document.querySelector('h2').textContent = "This doesn't mean you're blocked. Try playing from another page instead.";
}
return;
}
scriptElem.onload(() => {anitrackerKwikLoad(window.location.origin + window.location.pathname)});
}
function anitrackerKwikLoad(url) {
if (kwikDLPageRegex.test(url)) {
if (initialStorage.settings.autoDownload === false) return;
$(`
<div style="width:100%;height:100%;background-color:rgba(0, 0, 0, 0.9);position:fixed;z-index:999;display:flex;justify-content:center;align-items:center;" id="anitrackerKwikDL">
<span style="color:white;font-size:3.5em;font-weight:bold;">[AnimePahe Improvements] Downloading...</span>
</div>`).prependTo(document.body);
if ($('form').length > 0) {
$('form').submit();
setTimeout(() => {$('#anitrackerKwikDL').remove()}, 1500);
}
else new MutationObserver(function(mutationList, observer) {
if ($('form').length > 0) {
observer.disconnect();
$('form').submit();
setTimeout(() => {$('#anitrackerKwikDL').remove()}, 1500);
}
}).observe(document.body, { childList: true, subtree: true });
return;
}
// Needs to have this indentation
const _css = `
.anitracker-loading {
background: none!important;
border: 12px solid rgba(130,130,130,0.7);
border-top-color: #00d1b2;
border-radius: 50%;
animation: spin 1.2s linear infinite;
translate: -50% -50%;
width: 80px;
height: 80px;
}
.anitracker-message {
width:50%;
height:10%;
position:absolute;
background-color:rgba(0,0,0,0.5);
justify-content:center;
align-items:center;
margin-top:1.5%;
border-radius:20px;
}
.anitracker-message>span {
color: white;
font-size: 2.5em;
}
.anitracker-progress-tooltip {
width: 219px;
padding: 5px;
opacity:0;
position: absolute;
left:0%;
bottom: 100%;
background-color: rgba(255,255,255,0.88);
border-radius: 8px;
transition: translate .2s ease .1s,scale .2s ease .1s,opacity .1s ease .05s;
transform: translate(-50%,0);
user-select: none;
pointer-events: none;
z-index: 2;
}
.anitracker-progress-image {
height: 100%;
width: 100%;
background-color: gray;
display:flex;
flex-direction: column;
align-items: center;
overflow: hidden;
border-radius: 5px;
}
.anitracker-progress-image>img {
width: 100%;
}
.anitracker-progress-image>span {
font-size: .9em;
bottom: 5px;
position: fixed;
background-color: rgba(0,0,0,0.7);
border-radius: 3px;
padding: 0 4px 0 4px;
}
.anitracker-skip-button {
position: absolute;
left: 8%;
bottom: 10%;
color: white;
background-color: rgba(100,100,100,0.6);
z-index: 1;
border: 3px solid white;
border-radius: 8px;
padding: 10px 24px;
transition: .3s;
}
.anitracker-skip-button:hover, .anitracker-skip-button:focus-visible {
background-color: rgba(0,0,0,0.75);
}
.anitracker-skip-button:focus-visible {
outline: 3px dotted #00b3ff;
}
.anitracker-seek-points {
width: 100%;
bottom: 0;
height: 100%;
position: absolute;
display: flex;
align-items: center;
}
.anitracker-seek-points>i {
position: absolute;
width: 5px;
height: 5px;
border-radius: 2px;
background-color: #1a9166;
pointer-events: none;
z-index: 2;
translate: -50% 0;
}
.plyr--hide-controls>.anitracker-hide-control {
opacity: 0!important;
pointer-events: none!important;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}`;
applyCssSheet(_css);
if ($('.anitracker-message').length > 0) {
console.log("[AnimePahe Improvements (Player)] Script was reloaded.");
return;
}
$('button.plyr__controls__item:nth-child(1)').hide();
$('.plyr__progress__container').hide();
$('.plyr__control--overlaid').hide();
$(`
<div class="anitracker-loading plyr__control--overlaid">
<span class="plyr__sr-only">Loading...</span>
</div>`).appendTo('.plyr--video');
const player = $('#kwikPlayer')[0];
function getVideoInfo() {
const fileName = document.getElementsByClassName('ss-label')[0].textContent;
const nameParts = fileName.split('_');
let name = '';
for (let i = 0; i < nameParts.length; i++) {
const part = nameParts[i];
if (part.trim() === 'AnimePahe') {
i ++;
continue;
}
if (part === 'Dub' && i >= 1 && [2,3,4,5].includes(nameParts[i-1].length)) break;
if (/\d{2}/.test(part) && i >= 1 && nameParts[i-1] === '-') break;
name += nameParts[i-1] + ' ';
}
return {
animeName: name.slice(0, -1),
episodeNum: +/^AnimePahe_.+_-_([\d\.]{2,})/.exec(fileName)[1]
};
}
$(`<div class="anitracker-seek-points"></div>`).appendTo('.plyr__progress');
function setSeekPoints(seekPoints) {
for (const p of seekPoints) {
$(`<i style="left: ${p}%"></i>`).appendTo('.anitracker-seek-points');
}
}
var timestamps = [];
async function getAnidbIdFromTitle(title) {
return new Promise((resolve) => {
const req = new XMLHttpRequest();
req.open('GET', 'https://raw.githubusercontent.com/c032/anidb-animetitles-archive/refs/heads/main/data/animetitles.json', true);
req.onload = () => {
if (req.status !== 200) {
resolve(false);
return
};
const data = req.response.split('\n');
let anidbId = undefined;
for (const anime of data) {
const obj = JSON.parse(anime);
if (obj.titles.find(a => a.title === title) === undefined) continue;
anidbId = obj.id;
break;
}
resolve(anidbId);
};
req.send();
});
}
async function getTimestamps(anidbId, episode) {
return new Promise((resolve) => {
const req = new XMLHttpRequest();
req.open('GET', 'https://raw.githubusercontent.com/Ellivers/open-anime-timestamps/refs/heads/master/timestamps.json', true); // Timestamp data
req.onload = () => {
if (req.status !== 200) {
resolve(false);
return
};
const data = JSON.parse(req.response)[anidbId];
if (data === undefined) {
resolve(false);
return;
}
const episodeData = data.find(e => e.episode_number === episode);
if (episodeData !== undefined) {
console.log('[AnimePahe Improvements] Found timestamp data for episode.');
}
else {
resolve(false);
return;
}
const duration = player.duration;
let timestampData = [
{
type: "recap",
start: episodeData.recap.start,
end: episodeData.recap.end
},
{
type: "opening",
start: episodeData.opening.start,
end: episodeData.opening.end
},
{
type: "ending",
start: episodeData.ending.start,
end: episodeData.ending.end
},
{
type: "preview",
start: episodeData.preview_start,
end: duration
}
];
const seekPoints = [];
for (const t of timestampData) {
if (t.start === -1) continue;
const percentage = (t.start / duration) * 100;
seekPoints.push(percentage);
}
// Filter off unusable timestamps
timestampData = timestampData.filter(t => t.start !== -1 && (t.end !== -1 || t.type === 'preview'));
resolve({
seekPoints: seekPoints,
timestamps: timestampData
});
}
req.send();
});
}
function updateStoredPlaybackSpeed(speed) {
const storage = getStorage();
const vidInfo = getVideoInfo();
const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage);
if (storedVideoTime === undefined) return;
const storedPlaybackSpeed = storage.videoSpeed.find(a => a.animeId === storedVideoTime.animeId);
if (speed === 1 && storedVideoTime.animeId !== undefined) {
if (storedPlaybackSpeed === undefined) return;
storage.videoSpeed = storage.videoSpeed.filter(a => a.animeId !== storedVideoTime.animeId);
saveData(storage);
return;
}
if (storedPlaybackSpeed === undefined) {
storage.videoSpeed.push({
animeId: storedVideoTime.animeId,
animeName: vidInfo.animeName,
speed: speed
});
if (storage.videoSpeed.length > 256) storage.videoSpeed.splice(0,1);
}
else storedPlaybackSpeed.speed = speed;
saveData(storage);
}
function updateStoredTime() {
const currentTime = player.currentTime;
const storage = getStorage();
// Delete the storage entry
if (player.duration - currentTime <= 20) {
const videoInfo = getVideoInfo();
storage.videoTimes = storage.videoTimes.filter(a => !(a.animeName === videoInfo.animeName && a.episodeNum === videoInfo.episodeNum));
saveData(storage);
return;
}
if (waitingState.idRequest === 1) return;
const vidInfo = getVideoInfo();
const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage);
if (storedVideoTime === undefined) {
if (![-1,0].includes(waitingState.idRequest)) { // If the video has loaded (>0) and getting the ID has not failed (-1)
waitingState.idRequest = 1;
sendMessage({action: "id_request"});
setTimeout(() => {
if (waitingState.idRequest === 1) {
waitingState.idRequest = -1; // Failed to get the anime ID from the main page within 2 seconds
updateStoredTime();
}
}, 2000);
return;
}
const vidInfo = getVideoInfo();
storage.videoTimes.push({
videoUrls: [url],
time: player.currentTime,
animeName: vidInfo.animeName,
episodeNum: vidInfo.episodeNum
});
if (storage.videoTimes.length > 1000) {
storage.videoTimes.splice(0,1);
}
saveData(storage);
return;
}
storedVideoTime.time = player.currentTime;
saveData(storage);
}
if (initialStorage.videoTimes === undefined) {
const storage = getStorage();
storage.videoTimes = [];
saveData(storage);
}
// For message requests from the main page
// -1: failed
// 0: hasn't started
// 1: waiting
// 2: succeeded
const waitingState = {
idRequest: 0,
videoUrlRequest: 0,
anidbIdRequest: 0
};
// Messages received from main page
window.onmessage = function(e) {
const storage = getStorage();
const vidInfo = getVideoInfo();
const data = e.data;
const action = data.action;
if (action === 'id_response' && waitingState.idRequest === 1) {
storage.videoTimes.push({
videoUrls: [url],
time: 0,
animeName: vidInfo.animeName,
episodeNum: vidInfo.episodeNum,
animeId: data.id
});
if (storage.videoTimes.length > 1000) {
storage.videoTimes.splice(0,1);
}
saveData(storage);
waitingState.idRequest = 2;
const storedPlaybackSpeed = storage.videoSpeed.find(a => a.animeId === data.id);
if (storedPlaybackSpeed !== undefined) {
setSpeed(storedPlaybackSpeed.speed);
}
waitingState.anidbIdRequest = 1;
sendMessage({action:"anidb_id_request",id:data.id});
return;
}
else if (action === 'anidb_id_response' && waitingState.anidbIdRequest === 1) {
waitingState.anidbIdRequest = 2;
let anidbId = data.id;
if (anidbId === undefined) {
const episode = storage.linkList.find(e => e.type === 'episode' && e.animeId === data.originalId);
if (episode === undefined) return;
getAnidbIdFromTitle(episode.animeName).then(response => {
anidbId = response;
});
}
if (anidbId === undefined) return;
getTimestamps(anidbId, vidInfo.episodeNum).then(response => {
const storage = getStorage();
const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage);
if (response === false) {
storedVideoTime.hasTimestamps = false;
saveData(storage);
return;
}
if (storage.settings.seekPoints) setSeekPoints(response.seekPoints);
if (storage.settings.skipButton) timestamps = response.timestamps;
storedVideoTime.hasTimestamps = true;
storedVideoTime.timestampData = response;
saveData(storage);
});
}
else if (action === 'video_url_response' && waitingState.videoUrlRequest === 1) {
waitingState.videoUrlRequest = 2;
const request = new XMLHttpRequest();
request.open('GET', data.url, true);
request.onload = () => {
if (request.status !== 200) {
console.error('[AnimePahe Improvements] Could not get kwik page for video source');
return;
}
const pageElements = Array.from($(request.response)); // Elements that are not buried cannot be found with jQuery.find()
const hostInfo = (() => {
for (const link of pageElements.filter(a => a.tagName === 'LINK')) {
const href = $(link).attr('href');
if (!href.includes('vault')) continue;
const result = /vault-(\d+)\.(\w+\.\w+)$/.exec(href);
return {
vaultId: result[1],
hostName: result[2]
}
break;
}
})();
const searchInfo = (() => {
for (const script of pageElements.filter(a => a.tagName === 'SCRIPT')) {
if ($(script).attr('url') !== undefined || !$(script).text().startsWith('eval')) continue;
const result = /(\w{64})\|((?:\w+\|){4,5})https/.exec($(script).text());
let extraNumber = undefined;
result[2].split('|').forEach(a => {if (/\d{2}/.test(a)) extraNumber = a;}); // Some number that's needed for the url (doesn't always exist here)
if (extraNumber === undefined) {
const result2 = /q=\\'\w+:\/{2}\w+\-\w+\.\w+\.\w+\/((?:\w+\/)+)/.exec($(script).text());
result2[1].split('/').forEach(a => {if (/\d{2}/.test(a) && a !== hostInfo.vaultId) extraNumber = a;});
}
if (extraNumber === undefined) {
const result2 = /source\|(\d{2})\|ended/.exec($(script).text());
if (result2 !== null) extraNumber = result2[1];
}
return {
part1: extraNumber,
part2: result[1]
};
break;
}
})();
if (searchInfo.part1 === undefined) {
console.error('[AnimePahe Improvements] Could not find "extraNumber" from ' + data.url);
return;
}
setupSeekThumbnails(`https://vault-${hostInfo.vaultId}.${hostInfo.hostName}/stream/${hostInfo.vaultId}/${searchInfo.part1}/${searchInfo.part2}/uwu.m3u8`);
};
request.send();
}
else if (action === 'change_time') {
if (data.time !== undefined) player.currentTime = data.time;
}
else if (action === 'key') {
if ([' ','k'].includes(data.key)) {
if (player.paused) player.play();
else player.pause();
}
else if (data.key === 'ArrowLeft') {
player.currentTime = Math.max(0, player.currentTime - 5);
return;
}
else if (data.key === 'ArrowRight') {
player.currentTime = Math.min(player.duration, player.currentTime + 5);
return;
}
else if (/^\d$/.test(data.key)) {
player.currentTime = (player.duration/10)*(+data.key);
return;
}
else if (data.key === 'm') player.muted = !player.muted;
else $(player).trigger('keydown', {
key: data.key
});
}
else if (action === 'setting_changed') {
const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage);
if (data.type === 'seek_points' && storedVideoTime.hasTimestamps === true) {
if (data.value === true && $('.anitracker-seek-points>i').length === 0) setSeekPoints(storedVideoTime.timestampData.seekPoints);
else if (data.value === false) $('.anitracker-seek-points>i').remove();
}
else if (data.type === 'skip_button' && storedVideoTime.hasTimestamps === true) {
if (data.value === true) {
timestamps = storedVideoTime.timestampData.timestamps;
checkActiveTimestamps();
}
else {
setSkipBtnVisibility(false);
timestamps = [];
}
}
}
};
$('.plyr--full-ui').attr('tabindex','1');
let skipBtnVisible = false;
function setSkipBtnVisibility(on) {
const elem = $('.anitracker-skip-button');
if (on && !skipBtnVisible) {
elem.css('opacity','1').css('pointer-events','').css('translate','');
elem.attr('tabindex','2');
skipBtnVisible = true;
}
else if (!on && skipBtnVisible) {
elem.css('opacity','0').css('pointer-events','none').css('translate','-50%');
elem.removeClass('anitracker-hide-control');
elem.attr('tabindex','-1');
elem.off('click');
skipBtnVisible = false;
}
}
const skipTexts = {
'recap': 'Skip Recap',
'opening': 'Skip Opening',
'ending': 'Skip Ending',
'preview': 'Skip to End'
}
function checkActiveTimestamps(time = player.currentTime) {
if (timestamps.length === 0) return;
let activeTimestamp;
for (const t of timestamps) {
if (time > t.start && time < (t.end - 2)) {
activeTimestamp = t;
break;
}
}
if (activeTimestamp === undefined) {
setSkipBtnVisibility(false);
return;
}
const elem = $('.anitracker-skip-button');
const text = skipTexts[activeTimestamp.type] || 'Skip Section';
if (text === elem.text() && skipBtnVisible) {
if (time - activeTimestamp.start > 4) {
elem.addClass('anitracker-hide-control');
}
return;
}
elem.text(text);
setSkipBtnVisibility(true);
elem.off('click');
elem.on('click', () => {
player.currentTime = activeTimestamp.end - 2;
setSkipBtnVisibility(false);
});
}
player.addEventListener('loadeddata', function loadVideoData() {
const storage = getStorage();
const vidInfo = getVideoInfo();
const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage);
if (storedVideoTime !== undefined) {
player.currentTime = Math.max(0, Math.min(storedVideoTime.time, player.duration));
if (storedVideoTime.hasTimestamps) {
if (storage.settings.skipButton) timestamps = storedVideoTime.timestampData.timestamps;
if (storage.settings.seekPoints) setSeekPoints(storedVideoTime.timestampData.seekPoints);
}
if (!storedVideoTime.videoUrls.includes(url)) {
storedVideoTime.videoUrls.push(url);
saveData(storage);
}
const storedPlaybackSpeed = storage.videoSpeed.find(a => a.animeId === storedVideoTime.animeId);
if (storedPlaybackSpeed !== undefined) {
setSpeed(storedPlaybackSpeed.speed);
}
else player.playbackRate = 1;
}
else {
player.playbackRate = 1;
waitingState.idRequest = 1;
sendMessage({action: "id_request"});
setTimeout(() => {
if (waitingState.idRequest === 1) {
waitingState.idRequest = -1; // Failed to get the anime ID from the main page within 2 seconds
updateStoredTime();
}
}, 2000);
finishedLoading();
}
const timeArg = Array.from(new URLSearchParams(window.location.search)).find(a => a[0] === 'time');
if (timeArg !== undefined) {
const newTime = +timeArg[1];
if (storedVideoTime === undefined || (storedVideoTime !== undefined && Math.floor(storedVideoTime.time) === Math.floor(newTime)) || (storedVideoTime !== undefined &&
confirm(`[AnimePahe Improvements]\n\nYou already have saved progress on this video (${secondsToHMS(storedVideoTime.time)}). Do you want to overwrite it and go to ${secondsToHMS(newTime)}?`))) {
player.currentTime = Math.max(0, Math.min(newTime, player.duration));
}
window.history.replaceState({}, document.title, url);
}
player.removeEventListener('loadeddata', loadVideoData);
// Set up events
let lastTimeUpdate = 0;
player.addEventListener('timeupdate', function() {
const currentTime = player.currentTime;
checkActiveTimestamps(currentTime);
if (Math.trunc(currentTime) % 10 === 0 && player.currentTime - lastTimeUpdate > 9) {
updateStoredTime();
lastTimeUpdate = player.currentTime;
}
});
player.addEventListener('pause', () => {
updateStoredTime();
});
player.addEventListener('seeked', () => {
updateStoredTime();
checkActiveTimestamps();
finishedLoading();
});
player.addEventListener('ratechange', () => {
if (player.readyState > 2) updateStoredPlaybackSpeed(player.playbackRate);
});
});
function getFrame(video, time, dimensions) {
return new Promise((resolve) => {
video.onseeked = () => {
const canvas = document.createElement('canvas');
canvas.height = dimensions.y;
canvas.width = dimensions.x;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
resolve(canvas.toDataURL('image/png'));
};
try {
video.currentTime = time;
}
catch (e) {
console.error(time, e);
}
});
}
const settingsContainerId = (() => {
for (const elem of $('.plyr__menu__container')) {
const regex = /plyr\-settings\-(\d+)/.exec(elem.id);
if (regex === null) continue;
return regex[1];
}
return undefined;
})();
function setupSeekThumbnails(videoSource) {
const resolution = 167;
const bgVid = document.createElement('video');
bgVid.height = resolution;
bgVid.onloadeddata = () => {
const fullDuration = bgVid.duration;
const timeBetweenThumbnails = fullDuration/(24*6); // Just something arbitrary that seems good
const thumbnails = [];
const aspectRatio = bgVid.videoWidth / bgVid.videoHeight;
const aspectRatioCss = `${bgVid.videoWidth} / ${bgVid.videoHeight}`;
$('.plyr__progress .plyr__tooltip').remove();
$(`
<div class="anitracker-progress-tooltip" style="aspect-ratio: ${aspectRatioCss};">
<div class="anitracker-progress-image">
<img style="display: none; aspect-ratio: ${aspectRatioCss};">
<span>0:00</span>
</div>
</div>`).insertAfter(`progress`);
$('.anitracker-progress-tooltip img').on('load', () => {
$('.anitracker-progress-tooltip img').css('display', 'block');
});
const toggleVisibility = (on) => {
if (on) $('.anitracker-progress-tooltip').css('opacity', '1').css('scale','1').css('translate','');
else $('.anitracker-progress-tooltip').css('opacity', '0').css('scale','0.75').css('translate','-12.5% 20px');
};
const elem = $('.anitracker-progress-tooltip');
let currentTime = 0;
new MutationObserver(function(mutationList, observer) {
if ($('.plyr--full-ui').hasClass('plyr--hide-controls') || !$(`#plyr-seek-${settingsContainerId}`)[0].matches(':hover')) {
toggleVisibility(false);
return;
}
toggleVisibility(true);
const seekValue = $(`#plyr-seek-${settingsContainerId}`).attr('seek-value');
const time = seekValue !== undefined ? Math.min(Math.max(Math.trunc(fullDuration*(+seekValue/100)), 0), fullDuration) : Math.trunc(player.currentTime);
const roundedTime = Math.trunc(time/timeBetweenThumbnails)*timeBetweenThumbnails;
const timeSlot = Math.trunc(time/timeBetweenThumbnails);
elem.find('span').text(secondsToHMS(time));
elem.css('left', seekValue + '%');
if (roundedTime === Math.trunc(currentTime/timeBetweenThumbnails)*timeBetweenThumbnails) return;
const cached = thumbnails.find(a => a.time === timeSlot);
if (cached !== undefined) {
elem.find('img').attr('src', cached.data);
}
else {
elem.find('img').css('display', 'none');
getFrame(bgVid, roundedTime, {y: resolution, x: resolution*aspectRatio}).then((response) => {
thumbnails.push({
time: timeSlot,
data: response
});
elem.find('img').css('display', 'none');
elem.find('img').attr('src', response);
});
}
currentTime = time;
}).observe($(`#plyr-seek-${settingsContainerId}`)[0], { attributes: true });
$(`#plyr-seek-${settingsContainerId}`).on('mouseleave', () => {
toggleVisibility(false);
});
}
const hls2 = new Hls({
maxBufferLength: 0.1,
backBufferLength: 0,
capLevelToPlayerSize: true,
maxAudioFramesDrift: Infinity
});
hls2.loadSource(videoSource);
hls2.attachMedia(bgVid);
}
// Thumbnails when seeking
if (Hls.isSupported() && initialStorage.settings.seekThumbnails !== false) {
sendMessage({action:"video_url_request"});
waitingState.videoUrlRequest = 1;
setTimeout(() => {
if (waitingState.videoUrlRequest === 2) return;
waitingState.videoUrlRequest = -1;
if (typeof hls !== "undefined") setupSeekThumbnails(hls.url);
}, 500);
}
function finishedLoading() {
if ($('.anitracker-loading').length === 0) return;
$('.anitracker-loading').remove();
$('button.plyr__controls__item:nth-child(1)').show();
$('.plyr__progress__container').show();
$('.plyr__control--overlaid').show();
const storage = getStorage();
if (storage.settings.autoPlayVideo === true) player.play();
}
let messageTimeout = undefined;
function showMessage(text) {
$('.anitracker-message span').text(text);
$('.anitracker-message').css('display', 'flex');
clearTimeout(messageTimeout);
messageTimeout = setTimeout(() => {
$('.anitracker-message').hide();
}, 1000);
}
const frametime = 1 / 24;
let funPitch = "";
$(document).on('keydown', function(e, other = undefined) {
const key = e.key || other.key;
if (key === 'ArrowUp') {
changeSpeed(e, -1); // The changeSpeed function only works if ctrl is being held
return;
}
if (key === 'ArrowDown') {
changeSpeed(e, 1);
return;
}
if (e.shiftKey && ['l','L'].includes(key)) {
showMessage('Loop: ' + (player.loop ? 'Off' : 'On'));
player.loop = !player.loop;
return;
}
if (e.shiftKey && ['n','N'].includes(key)) {
sendMessage({action: "next"});
return;
}
if (e.shiftKey && ['p','P'].includes(key)) {
sendMessage({action: "previous"});
return;
}
if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) return; // Prevents special keys for the rest of the keybinds
if (key === 'j') {
player.currentTime = Math.max(0, player.currentTime - 10);
return;
}
else if (key === 'l') {
player.currentTime = Math.min(player.duration, player.currentTime + 10);
setTimeout(() => {
player.loop = false;
}, 5);
return;
}
else if (/^Numpad\d$/.test(e.code)) {
player.currentTime = (player.duration/10)*(+e.code.replace('Numpad', ''));
return;
}
if (!(player.currentTime > 0 && !player.paused && !player.ended && player.readyState > 2)) {
if (key === ',') {
player.currentTime = Math.max(0, player.currentTime - frametime);
return;
}
else if (key === '.') {
player.currentTime = Math.min(player.duration, player.currentTime + frametime);
return;
}
}
funPitch += key;
if (funPitch === 'crazy') {
player.preservesPitch = !player.preservesPitch;
showMessage(player.preservesPitch ? 'Off' : 'Change speed ;D');
funPitch = "";
return;
}
if (!"crazy".startsWith(funPitch)) {
funPitch = "";
}
sendMessage({
action: "key",
key: key
});
});
// Ctrl+scrolling to change speed
$(`
<button class="anitracker-skip-button" tabindex="-1" style="opacity:0;pointer-events:none;translate:-50%;" aria-label="Skip section"><span>Skip Section</span></button>
<div class="anitracker-message" style="display:none;">
<span>2.0x</span>
</div>`).appendTo($(player).parents().eq(1));
jQuery.event.special.wheel = {
setup: function( _, ns, handle ){
this.addEventListener("wheel", handle, { passive: false });
}
};
const defaultSpeeds = player.plyr.options.speed;
function changeSpeed(e, delta) {
if (!e.ctrlKey) return;
e.preventDefault();
if (delta == 0) return;
const speedChange = e.shiftKey ? 0.05 : 0.1;
setSpeed(player.playbackRate + speedChange * (delta > 0 ? -1 : 1));
}
function setSpeed(speed) {
if (speed > 0) player.playbackRate = Math.round(speed * 100) / 100;
showMessage(player.playbackRate + "x");
if (defaultSpeeds.includes(player.playbackRate)) {
$('.anitracker-custom-speed-btn').remove();
}
else if ($('.anitracker-custom-speed-btn').length === 0) {
$(`#plyr-settings-${settingsContainerId}-speed>div>button`).attr('aria-checked','false');
$(`
<button type="button" role="menuitemradio" class="plyr__control anitracker-custom-speed-btn" aria-checked="true"><span>Custom</span></button>
`).prependTo(`#plyr-settings-${settingsContainerId}-speed>div`);
for (const elem of $(`#plyr-settings-${settingsContainerId}-home>div>`)) {
if (!/^Speed/.test($(elem).children('span')[0].textContent)) continue;
$(elem).find('span')[1].textContent = "Custom";
}
}
}
$(`#plyr-settings-${settingsContainerId}-speed>div>button`).on('click', (e) => {
$('.anitracker-custom-speed-btn').remove();
});
$(document).on('wheel', function(e) {
changeSpeed(e, e.originalEvent.deltaY);
});
}
return;
}
if ($() !== null) anitrackerLoad(window.location.origin + window.location.pathname + window.location.search);
else {
document.querySelector('head > link:nth-child(10)').onload(() => {anitrackerLoad(window.location.origin + window.location.pathname + window.location.search)});
}
function anitrackerLoad(url) {
if ($('#anitracker-modal').length > 0) {
console.log("[AnimePahe Improvements] Script was reloaded.");
return;
}
if (initialStorage.settings.hideThumbnails === true) {
hideThumbnails();
}
function windowOpen(url, target = '_blank') {
$(`<a href="${url}" target="${target}"></a>`)[0].click();
}
(function($) {
$.fn.changeElementType = function(newType) {
let attrs = {};
$.each(this[0].attributes, function(idx, attr) {
attrs[attr.nodeName] = attr.nodeValue;
});
this.replaceWith(function() {
return $("<" + newType + "/>", attrs).append($(this).contents());
});
};
$.fn.replaceClass = function(oldClass, newClass) {
this.removeClass(oldClass).addClass(newClass);
};
})(jQuery);
// -------- AnimePahe Improvements CSS ---------
const animationTimes = {
modalOpen: 0.2,
fadeIn: 0.2
};
const _css = `
#anitracker {
display: flex;
flex-direction: row;
gap: 15px 7px;
align-items: center;
flex-wrap: wrap;
}
.anitracker-index {
align-items: end !important;
}
#anitracker>span {align-self: center;\n}
#anitracker-modal {
position: fixed;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.6);
z-index: 20;
display: none;
}
#anitracker-modal-content {
max-height: 90%;
background-color: var(--dark);
margin: auto auto auto auto;
border-radius: 20px;
display: flex;
padding: 20px;
z-index:50;
}
#anitracker-modal-close {
font-size: 2.5em;
margin: 3px 10px;
cursor: pointer;
height: 1em;
}
#anitracker-modal-close:hover,#anitracker-modal-close:focus-visible {
color: rgb(255, 0, 108);
}
#anitracker-modal-body {
padding: 10px;
overflow-x: hidden;
}
#anitracker-modal-body .anitracker-switch {margin-bottom: 2px;\n}
.anitracker-big-list-item {
list-style: none;
border-radius: 10px;
margin-top: 5px;
}
.anitracker-big-list-item>a {
font-size: 0.875rem;
display: block;
padding: 5px 15px;
color: rgb(238, 238, 238);
text-decoration: none;
}
.anitracker-big-list-item img {
margin: auto 0px;
width: 50px;
height: 50px;
border-radius: 100%;
}
.anitracker-big-list-item .anitracker-main-text {
font-weight: 700;
color: rgb(238, 238, 238);
}
.anitracker-big-list-item .anitracker-subtext {
font-size: 0.75rem;
color: rgb(153, 153, 153);
}
.anitracker-big-list-item:hover .anitracker-main-text {
color: rgb(238, 238, 238);
}
.anitracker-big-list-item:hover .anitracker-subtext {
color: rgb(238, 238, 238);
}
.anitracker-big-list-item:hover {
background-color: #111;
}
.anitracker-big-list-item:focus-within .anitracker-main-text {
color: rgb(238, 238, 238);
}
.anitracker-big-list-item:focus-within .anitracker-subtext {
color: rgb(238, 238, 238);
}
.anitracker-big-list-item:focus-within {
background-color: #111;
}
.anitracker-hide-thumbnails .anitracker-thumbnail img {display: none;\n}
.anitracker-hide-thumbnails .anitracker-thumbnail {
border: 10px solid rgb(32, 32, 32);
aspect-ratio: 16/9;
}
.anitracker-hide-thumbnails .episode-snapshot img {
display: none;
}
.anitracker-hide-thumbnails .episode-snapshot {
border: 4px solid var(--dark);
}
.anitracker-download-spinner {display: inline;\n}
.anitracker-download-spinner .spinner-border {
height: 0.875rem;
width: 0.875rem;
}
.anitracker-dropdown-content {
display: none;
position: absolute;
min-width: 100px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
max-height: 400px;
overflow-y: auto;
overflow-x: hidden;
background-color: #171717;
}
.anitracker-dropdown-content button {
color: white;
padding: 12px 16px;
text-decoration: none;
display: block;
width:100%;
background-color: #171717;
border: none;
margin: 0;
}
.anitracker-dropdown-content button:hover, .anitracker-dropdown-content button:focus {background-color: black;\n}
.anitracker-active, .anitracker-active:hover, .anitracker-active:active {
color: white!important;
background-color: #d5015b!important;
}
.anitracker-dropdown-content a:hover {background-color: #ddd;\n}
.anitracker-dropdown:hover .anitracker-dropdown-content {display: block;\n}
.anitracker-dropdown:hover .anitracker-dropbtn {background-color: #bc0150;\n}
#pickDownload span, #scrollArea span {
cursor: pointer;
font-size: 0.875rem;
}
.anitracker-expand-data-icon {
font-size: 24px;
float: right;
margin-top: 6px;
margin-right: 8px;
}
.anitracker-modal-list-container {
background-color: rgb(40,45,50);
margin-bottom: 10px;
border-radius: 12px;
}
.anitracker-storage-data {
background-color: rgb(40,45,50);
border-radius: 12px;
cursor: pointer;
position: relative;
z-index: 1;
}
.anitracker-storage-data:focus {
box-shadow: 0 0 0 .2rem rgb(255, 255, 255);
}
.anitracker-storage-data span {
display:inline-block;
font-size: 1.4em;
font-weight: bold;
}
.anitracker-storage-data, .anitracker-modal-list {
padding: 10px;
}
.anitracker-modal-list-entry {margin-top: 8px;\n}
.anitracker-modal-list-entry a {text-decoration: underline;\n}
.anitracker-modal-list-entry:hover {background-color: rgb(30,30,30);\n}
.anitracker-relation-link {
text-overflow: ellipsis;
overflow: hidden;
}
#anitracker-cover-spinner .spinner-border {
width:2rem;
height:2rem;
}
.anime-cover {
display: flex;
justify-content: center;
align-items: center;
image-rendering: optimizequality;
}
.anitracker-filter-input {
width: 12.2rem;
display: inline-block;
cursor: text;
}
.anitracker-filter-input > div {
height:56px;
width:100%;
border-bottom: 2px solid #454d54;
overflow-y: auto;
}
.anitracker-filter-input.active > div {
border-color: rgb(213, 1, 91);
}
.anitracker-filter-rules {
background: black;
border: 1px solid #bbb;
color: #bbb;
padding: 5px;
float: right;
border-radius: 5px;
font-size: .8em;
width: 2em;
aspect-ratio: 1;
margin-bottom: -10px;
z-index: 1;
position: relative;
min-height: 0;
}
.anitracker-filter-rules>i {
vertical-align: super;
}
.anitracker-filter-rules.anitracker-active {
border-color: rgb(213, 1, 91);
}
.anitracker-filter-rules:hover, .anitracker-filter-rules:focus-visible {
background: white;
color: black;
border-color: white;
}
.anitracker-filter-input-search {
position: absolute;
max-width: 150px;
max-height: 45px;
min-width: 150px;
min-height: 45px;
overflow-wrap: break-word;
overflow-y: auto;
}
.anitracker-filter-input .placeholder {
color: #999;
position: absolute;
z-index: -1;
}
.anitracker-filter-icon {
padding: 0;
padding-right: 4px;
border-radius: 12px;
display: inline-block;
cursor: pointer;
border: 2px solid white;
margin-right: 5px;
transition: background-color .3s, border-color .3s;
vertical-align: text-top;
}
.anitracker-filter-icon>i {
margin: 2px;
font-size: .8em;
}
.anitracker-filter-icon.included {
background-color: rgba(20, 113, 30, 0.64);
border-color: rgb(62, 181, 62);
}
.anitracker-filter-icon.included>i {
color: rgb(83, 255, 83);
}
.anitracker-filter-icon.excluded {
background-color: rgba(187, 62, 62, 0.41);
border-color: #d75a5a;
}
.anitracker-filter-icon.excluded>i {
color: rgb(227, 96, 96);
}
.anitracker-filter-icon:hover {
border-color: white;
}
#anitracker-settings-invert-switch:checked ~ .custom-control-label::before {
border-color: red;
background-color: red;
}
#anitracker-settings-invert-switch:checked[disabled=""] ~ .custom-control-label::before {
border-color: #e88b8b;
background-color: #e88b8b;
}
.anitracker-text-input {
display: inline-block;
height: 1em;
line-break: anywhere;
min-width: 50px;
}
.anitracker-text-input-bar {
background: #333;
box-shadow: none;
color: #bbb;
}
.anitracker-text-input-bar:focus {
border-color: #d5015b;
background: none;
box-shadow: none;
color: #ddd;
}
.anitracker-text-input-bar[disabled=""] {
background: rgb(89, 89, 89);
border-color: gray;
cursor: not-allowed;
}
.anitracker-applied-filters {
display: inline-block;
}
.anitracker-placeholder {
color: gray;
}
.anitracker-filter-dropdown>button {
transition: background-color .3s;
}
.anitracker-filter-dropdown>button.included {
background-color: rgb(6, 130, 6);
}
.anitracker-filter-dropdown>button.included:focus {
border: 2px dashed rgb(141, 234, 141);
}
.anitracker-filter-dropdown>button.excluded {
background-color: rgb(117, 17, 17);
}
.anitracker-filter-dropdown>button.excluded:focus {
border: 2px dashed rgb(215, 90, 90);
}
.anitracker-filter-dropdown>button.anitracker-active:focus {
border: 2px dashed #ffd7eb;
}
#anitracker-season-copy-to-lower {
color:white;
margin-left:14px;
border-radius:5px;
}
.anitracker-filter-spinner.small {
display: inline-flex;
margin-left: 10px;
justify-content: center;
align-items: center;
vertical-align: bottom;
}
.anitracker-filter-spinner.screen {
width:100%;
height:100%;
background-color:rgba(0, 0, 0, 0.9);
position:fixed;
z-index:999;
display:flex;
justify-content:center;
align-items:center;
}
.anitracker-filter-spinner .spinner-border {
color:#d5015b;
}
.anitracker-filter-spinner.screen .spinner-border {
width:5rem;
height:5rem;
border-width: 10px;
}
.anitracker-filter-spinner>span {
position: absolute;
font-weight: bold;
}
.anitracker-filter-spinner.small>span {
font-size: .5em;
}
.anitracker-filter-rule-selection {
margin-bottom: 2px;
display: grid;
grid-template-columns: 1.5em 32% auto;
align-items: center;
grid-gap: 5px;
border-radius: 20px;
padding: 5px;
}
.anitracker-filter-rule-selection[disabled=""]>* {
opacity: 0.5;
pointer-events: none;
}
.anitracker-filter-rule-selection>i {
text-align: center;
border-radius: 35%;
padding: 2px;
aspect-ratio: 1;
}
.anitracker-filter-rule-selection>i::before {
vertical-align: middle;
}
.anitracker-filter-rule-selection>.fa-plus {
color: rgb(72, 223, 58);
background-color: #148214;
}
.anitracker-filter-rule-selection>.fa-minus {
color: #ff0000;
background-color: #911212;
}
.anitracker-filter-rule-selection button {
padding: 0;
width: 2.5em;
height: 2.5em;
background-color: var(--secondary);
border: 3px solid var(--dark);
border-radius: 35%;
outline: rgb(94, 96, 100) solid 3px;
margin: 5px;
color: white;
}
.anitracker-filter-rule-selection button.anitracker-active {
outline-color: rgb(213, 1, 91);
}
.anitracker-filter-rule-selection button:hover:not([disabled=""]), .anitracker-filter-rule-selection button:focus-visible:not([disabled=""]) {
outline-color: white;
}
.anitracker-flat-button {
padding-top: 0;
padding-bottom: 0;
}
.anitracker-list-btn {
height: 42px;
border-radius: 7px!important;
color: #ddd!important;
margin-left: 10px!important;
}
.anitracker-reverse-order-button {
font-size: 2em;
}
.anitracker-reverse-order-button::after {
vertical-align: 20px;
}
.anitracker-reverse-order-button.anitracker-up::after {
border-top: 0;
border-bottom: .3em solid;
vertical-align: 22px;
}
#anitracker-time-search-button {
float: right;
}
#anitracker-time-search-button svg {
width: 24px;
vertical-align: bottom;
}
.anitracker-season-group {
display: grid;
grid-template-columns: 10% 30% 20% 10%;
margin-bottom: 5px;
}
.anitracker-season-group .btn-group {
margin-left: 5px;
}
.anitracker-season-group>span {
align-self: center;
}
a.youtube-preview::before {
-webkit-transition: opacity .2s linear!important;
-moz-transition: opacity .2s linear!important;
transition: opacity .2s linear!important;
}
.anitracker-replaced-cover {background-position-y: 25%;\n}
.anitracker-text-button {
color:#d5015b;
cursor:pointer;
user-select:none;
}
.anitracker-text-button:hover {
color:white;
}
.nav-search {
float: left!important;
}
.anitracker-title-icon {
margin-left: 1rem!important;
opacity: .8!important;
color: #ff006c!important;
font-size: 2rem!important;
vertical-align: middle;
cursor: pointer;
padding: 0;
box-shadow: none!important;
}
.anitracker-title-icon:hover {
opacity: 1!important;
}
.anitracker-title-icon-check {
color: white;
margin-left: -.7rem!important;
font-size: 1rem!important;
vertical-align: super;
text-shadow: none;
opacity: 1!important;
}
.anitracker-header {
display: flex;
justify-content: left;
gap: 18px;
flex-grow: 0.05;
}
.anitracker-header-button {
color: white;
background: none;
border: 2px solid white;
border-radius: 5px;
width: 2rem;
}
.anitracker-header-button:hover {
border-color: #ff006c;
color: #ff006c;
}
.anitracker-header-button:focus {
border-color: #ff006c;
color: #ff006c;
}
.anitracker-header-notifications-circle {
color: rgb(255, 0, 108);
margin-left: -.3rem;
font-size: 0.7rem;
position: absolute;
}
.anitracker-notification-item .anitracker-main-text {
color: rgb(153, 153, 153);
}
.anitracker-notification-item-unwatched {
background-color: rgb(119, 62, 70);
}
.anitracker-notification-item-unwatched .anitracker-main-text {
color: white!important;
}
.anitracker-notification-item-unwatched .anitracker-subtext {
color: white!important;
}
.anitracker-watched-toggle {
font-size: 1.7em;
float: right;
margin-right: 5px;
margin-top: 5px;
cursor: pointer;
background-color: #592525;
padding: 5px;
border-radius: 5px;
}
.anitracker-watched-toggle:hover,.anitracker-watched-toggle:focus {
box-shadow: 0 0 0 .2rem rgb(255, 255, 255);
}
#anitracker-replace-cover {
z-index: 99;
right: 10px;
position: absolute;
bottom: 6em;
}
header.main-header nav .main-nav li.nav-item > a:focus {
color: #fff;
background-color: #bc0150;
}
.theatre-settings .dropup .btn:focus {
box-shadow: 0 0 0 .15rem rgb(100, 100, 100)!important;
}
.anitracker-episode-time {
margin-left: 5%;
font-size: 0.75rem!important;
cursor: default!important;
}
.anitracker-episode-time:hover {
text-decoration: none!important;
}
.index>* {
width: 100%;
}
@media screen and (min-width: 1375px) {
.theatre.anitracker-theatre-mode {
margin-top: 10px!important;
}
.theatre.anitracker-theatre-mode>* {
max-width: 81%!important;
}
}
@keyframes anitracker-modalOpen {
0% {
transform: scale(0.5);
}
50% {
transform: scale(1.07);
}
100% {
transform: scale(1);
}
}
@keyframes anitracker-fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes anitracker-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
`;
applyCssSheet(_css);
const optionSwitches = [
{
optionId: 'autoDelete',
switchId: 'auto-delete',
value: initialStorage.settings.autoDelete
},
{
optionId: 'theatreMode',
switchId: 'theatre-mode',
value: initialStorage.settings.theatreMode,
onEvent: () => {
theatreMode(true);
},
offEvent: () => {
theatreMode(false);
}
},
{
optionId: 'hideThumbnails',
switchId: 'hide-thumbnails',
value: initialStorage.settings.hideThumbnails,
onEvent: hideThumbnails,
offEvent: () => {
$('.main').removeClass('anitracker-hide-thumbnails');
}
},
{
optionId: 'bestQuality',
switchId: 'best-quality',
value: initialStorage.settings.bestQuality,
onEvent: bestVideoQuality
},
{
optionId: 'autoDownload',
switchId: 'auto-download',
value: initialStorage.settings.autoDownload
},
{
optionId: 'autoPlayNext',
switchId: 'autoplay-next',
value: initialStorage.settings.autoPlayNext
},
{
optionId: 'autoPlayVideo',
switchId: 'autoplay-video',
value: initialStorage.settings.autoPlayVideo
},
{
optionId: 'seekThumbnails',
switchId: 'seek-thumbnails',
value: initialStorage.settings.seekThumbnails
},
{
optionId: 'seekPoints',
switchId: 'seek-points',
value: initialStorage.settings.seekPoints,
onEvent: () => {
sendMessage({action:'setting_changed',type:'seek_points',value:true});
},
offEvent: () => {
sendMessage({action:'setting_changed',type:'seek_points',value:false});
}
},
{
optionId: 'skipButton',
switchId: 'skip-button',
value: initialStorage.settings.skipButton,
onEvent: () => {
sendMessage({action:'setting_changed',type:'skip_button',value:true});
},
offEvent: () => {
sendMessage({action:'setting_changed',type:'skip_button',value:false});
}
},
{
optionId: 'reduceMotion',
switchId: 'reduced-motion',
value: initialStorage.settings.reduceMotion
}];
const cachedAnimeData = [];
// Things that update when focusing this tab
$(document).on('visibilitychange', () => {
if (document.hidden) return;
updatePage();
});
function updatePage() {
updateSwitches();
const storage = getStorage();
const data = url.includes('/anime/') ? getAnimeData() : undefined;
if (data !== undefined) {
const isBookmarked = storage.bookmarks.find(a => a.id === data.id) !== undefined;
if (isBookmarked) $('.anitracker-bookmark-toggle .anitracker-title-icon-check').show();
else $('.anitracker-bookmark-toggle .anitracker-title-icon-check').hide();
const hasNotifications = storage.notifications.anime.find(a => a.id === data.id) !== undefined;
if (hasNotifications) $('.anitracker-notifications-toggle .anitracker-title-icon-check').show();
else $('.anitracker-notifications-toggle .anitracker-title-icon-check').hide();
}
if (!modalIsOpen() || $('.anitracker-view-notif-animes').length === 0) return;
for (const item of $('.anitracker-notification-item-unwatched')) {
const entry = storage.notifications.episodes.find(a => a.animeId === +$(item).attr('anime-data') && a.episode === +$(item).attr('episode-data') && a.watched === true);
if (entry === undefined) continue;
$(item).removeClass('anitracker-notification-item-unwatched');
const eye = $(item).find('.anitracker-watched-toggle');
eye.replaceClass('fa-eye', 'fa-eye-slash');
}
}
function theatreMode(on) {
if (on) $('.theatre').addClass('anitracker-theatre-mode');
else $('.theatre').removeClass('anitracker-theatre-mode');
}
function playAnimation(elem, anim, type = '', duration) {
return new Promise(resolve => {
elem.css('animation', `anitracker-${anim} ${duration || animationTimes[anim]}s forwards linear ${type}`);
if (animationTimes[anim] === undefined) resolve();
setTimeout(() => {
elem.css('animation', '');
resolve();
}, animationTimes[anim] * 1000);
});
}
let modalCloseFunction = closeModal;
// AnimePahe Improvements modal
function addModal() {
$(`
<div id="anitracker-modal" tabindex="-1">
<div id="anitracker-modal-content">
<i tabindex="0" id="anitracker-modal-close" class="fa fa-close" title="Close modal">
</i>
<div id="anitracker-modal-body"></div>
</div>
</div>`).insertBefore('.main-header');
$('#anitracker-modal').on('click', (e) => {
if (e.target !== e.currentTarget) return;
modalCloseFunction();
});
$('#anitracker-modal-close').on('click keydown', (e) => {
if (e.type === 'keydown' && e.key !== "Enter") return;
modalCloseFunction();
});
}
addModal();
function openModal(closeFunction = closeModal) {
if (closeFunction !== closeModal) $('#anitracker-modal-close').replaceClass('fa-close', 'fa-arrow-left');
else $('#anitracker-modal-close').replaceClass('fa-arrow-left', 'fa-close');
const storage = getStorage();
return new Promise(resolve => {
if (storage.settings.reduceMotion !== true) {
playAnimation($('#anitracker-modal-content'), 'modalOpen');
playAnimation($('#anitracker-modal'), 'fadeIn').then(() => {
$('#anitracker-modal').focus();
resolve();
});
}
else {
$('#anitracker-modal').focus();
resolve();
}
$('#anitracker-modal').css('display','flex');
modalCloseFunction = closeFunction;
});
}
function closeModal() {
const storage = getStorage();
if (storage.settings.reduceMotion === true || $('#anitracker-modal').css('animation') !== 'none') {
$('#anitracker-modal').hide();
return;
}
playAnimation($('#anitracker-modal'), 'fadeIn', 'reverse', 0.1).then(() => {
$('#anitracker-modal').hide();
});
}
function modalIsOpen() {
return $('#anitracker-modal').is(':visible');
}
let currentEpisodeTime = 0;
// Messages received from iframe
if (isEpisode()) {
window.onmessage = function(e) {
const data = e.data;
if (typeof(data) === 'number') {
currentEpisodeTime = Math.trunc(data);
return;
}
const action = data.action;
if (action === 'id_request') {
sendMessage({action:"id_response",id:getAnimeData().id});
}
else if (action === 'anidb_id_request') {
getAnidbId(data.id).then(result => {
sendMessage({action:"anidb_id_response",id:result,originalId:data.id});
});
}
else if (action === 'video_url_request') {
const selected = {
src: undefined,
res: undefined,
audio: undefined
}
for (const btn of $('#resolutionMenu>button')) {
const src = $(btn).data('src');
const res = +$(btn).data('resolution');
const audio = $(btn).data('audio');
if (selected.src !== undefined && selected.res < res) continue;
if (selected.audio !== undefined && audio === 'jp' && selected.res <= res) continue; // Prefer dubs, since they don't have subtitles
selected.src = src;
selected.res = res;
selected.audio = audio;
}
if (selected.src === undefined) {
console.error("[AnimePahe Improvements] Didn't find video URL");
return;
}
console.log('[AnimePahe Improvements] Found lowest resolution URL ' + selected.src);
sendMessage({action:"video_url_response", url:selected.src});
}
else if (action === 'key') {
if (data.key === 't') {
toggleTheatreMode();
}
}
else if (data === 'ended') {
const storage = getStorage();
if (storage.settings.autoPlayNext !== true) return;
const elem = $('.sequel a');
if (elem.length > 0) elem[0].click();
}
else if (action === 'next') {
const elem = $('.sequel a');
if (elem.length > 0) elem[0].click();
}
else if (action === 'previous') {
const elem = $('.prequel a');
if (elem.length > 0) elem[0].click();
}
};
}
function sendMessage(message) {
const iframe = $('.embed-responsive-item');
if (iframe.length === 0) return;
iframe[0].contentWindow.postMessage(message,'*');
}
function toggleTheatreMode() {
const storage = getStorage();
theatreMode(!storage.settings.theatreMode);
storage.settings.theatreMode = !storage.settings.theatreMode;
saveData(storage);
updateSwitches();
}
async function getAnidbId(paheId) {
return new Promise(resolve => {
const req = new XMLHttpRequest();
req.open('GET', `/a/${paheId}`, true);
req.onload = () => {
for (const link of $(req.response).find('.external-links a')) {
const elem = $(link);
if (elem.text() !== 'AniDB') continue;
resolve(/\/\/anidb.net\/anime\/(\d+)/.exec(elem.attr('href'))[1]);
}
resolve(undefined);
}
req.send();
})
}
function getSeasonValue(season) {
return ({winter:0, spring:1, summer:2, fall:3})[season.toLowerCase()];
}
function getSeasonName(season) {
return ["winter","spring","summer","fall"][season];
}
function stringSimilarity(s1, s2) {
let longer = s1;
let shorter = s2;
if (s1.length < s2.length) {
longer = s2;
shorter = s1;
}
const longerLength = longer.length;
if (longerLength == 0) {
return 1.0;
}
return (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength);
}
function editDistance(s1, s2) {
s1 = s1.toLowerCase();
s2 = s2.toLowerCase();
const costs = [];
for (let i = 0; i <= s1.length; i++) {
let lastValue = i;
for (let j = 0; j <= s2.length; j++) {
if (i == 0)
costs[j] = j;
else {
if (j > 0) {
let newValue = costs[j - 1];
if (s1.charAt(i - 1) != s2.charAt(j - 1))
newValue = Math.min(Math.min(newValue, lastValue),
costs[j]) + 1;
costs[j - 1] = lastValue;
lastValue = newValue;
}
}
}
if (i > 0)
costs[s2.length] = lastValue;
}
return costs[s2.length];
}
function searchForCollections() {
if ($('.search-results a').length === 0) return;
const baseName = $($('.search-results .result-title')[0]).text();
const request = new XMLHttpRequest();
request.open('GET', '/api?m=search&q=' + encodeURIComponent(baseName), true);
request.onload = () => {
if (request.readyState !== 4 || request.status !== 200 ) return;
response = JSON.parse(request.response).data;
if (response == undefined) return;
let seriesList = [];
for (const anime of response) {
if (stringSimilarity(baseName, anime.title) >= 0.42 || (anime.title.startsWith(baseName) && stringSimilarity(baseName, anime.title) >= 0.25)) {
seriesList.push(anime);
}
}
if (seriesList.length < 2) return;
seriesList = sortAnimesChronologically(seriesList);
displayCollection(baseName, seriesList);
};
request.send();
}
new MutationObserver(function(mutationList, observer) {
if (!searchComplete()) return;
searchForCollections();
}).observe($('.search-results-wrap')[0], { childList: true });
function searchComplete() {
return $('.search-results').length !== 0 && $('.search-results a').length > 0;
}
function displayCollection(baseName, seriesList) {
$(`
<li class="anitracker-collection" data-index="-1">
<a title="${toHtmlCodes(baseName + " - Collection")}" href="javascript:;">
<img src="${seriesList[0].poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" style="pointer-events: all !important;max-width: 30px;">
<img src="${seriesList[1].poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" style="pointer-events: all !important;max-width: 30px;left:30px;">
<div class="result-title">${baseName}</div>
<div class="result-status"><strong>Collection</strong> - ${seriesList.length} Entries</div>
</a>
</li>`).prependTo('.search-results');
function displayInModal() {
$('#anitracker-modal-body').empty();
$(`
<h4>Collection</h4>
<div class="anitracker-modal-list-container">
<div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;"></div>
</div>`).appendTo('#anitracker-modal-body');
for (const anime of seriesList) {
$(`
<div class="anitracker-big-list-item anitracker-collection-item">
<a href="/anime/${anime.session}" title="${toHtmlCodes(anime.title)}">
<img src="${anime.poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" alt="[Thumbnail of ${anime.title}]">
<div class="anitracker-main-text">${anime.title}</div>
<div class="anitracker-subtext"><strong>${anime.type}</strong> - ${anime.episodes > 0 ? anime.episodes : '?'} Episode${anime.episodes === 1 ? '' : 's'} (${anime.status})</div>
<div class="anitracker-subtext">${anime.season} ${anime.year}</div>
</a>
</div>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
}
openModal();
}
$('.anitracker-collection').on('click', displayInModal);
$('.input-search').on('keyup', (e) => {
if (e.key === "Enter" && $('.anitracker-collection').hasClass('selected')) displayInModal();
});
}
function getSeasonTimeframe(from, to) {
const filters = [];
for (let i = from.year; i <= to.year; i++) {
const start = i === from.year ? from.season : 0;
const end = i === to.year ? to.season : 3;
for (let d = start; d <= end; d++) {
filters.push({type: 'season_entry', value: {year: i, season: d}});
}
}
return filters;
}
const is404 = $('h1').text().includes('404');
if (!isRandomAnime() && initialStorage.cache !== undefined) {
const storage = getStorage();
delete storage.cache;
saveData(storage);
}
const filterSearchCache = {};
const filterValues = {
"genre":[
{"name":"Comedy","value":"comedy"},{"name":"Slice of Life","value":"slice-of-life"},{"name":"Romance","value":"romance"},{"name":"Ecchi","value":"ecchi"},{"name":"Drama","value":"drama"},
{"name":"Supernatural","value":"supernatural"},{"name":"Sports","value":"sports"},{"name":"Horror","value":"horror"},{"name":"Sci-Fi","value":"sci-fi"},{"name":"Action","value":"action"},
{"name":"Fantasy","value":"fantasy"},{"name":"Mystery","value":"mystery"},{"name":"Suspense","value":"suspense"},{"name":"Adventure","value":"adventure"},{"name":"Boys Love","value":"boys-love"},
{"name":"Girls Love","value":"girls-love"},{"name":"Hentai","value":"hentai"},{"name":"Gourmet","value":"gourmet"},{"name":"Erotica","value":"erotica"},{"name":"Avant Garde","value":"avant-garde"},
{"name":"Award Winning","value":"award-winning"}
],
"theme":[
{"name":"Adult Cast","value":"adult-cast"},{"name":"Anthropomorphic","value":"anthropomorphic"},{"name":"Detective","value":"detective"},{"name":"Love Polygon","value":"love-polygon"},
{"name":"Mecha","value":"mecha"},{"name":"Music","value":"music"},{"name":"Psychological","value":"psychological"},{"name":"School","value":"school"},{"name":"Super Power","value":"super-power"},
{"name":"Space","value":"space"},{"name":"CGDCT","value":"cgdct"},{"name":"Romantic Subtext","value":"romantic-subtext"},{"name":"Historical","value":"historical"},{"name":"Video Game","value":"video-game"},
{"name":"Martial Arts","value":"martial-arts"},{"name":"Idols (Female)","value":"idols-female"},{"name":"Idols (Male)","value":"idols-male"},{"name":"Gag Humor","value":"gag-humor"},{"name":"Parody","value":"parody"},
{"name":"Performing Arts","value":"performing-arts"},{"name":"Military","value":"military"},{"name":"Harem","value":"harem"},{"name":"Reverse Harem","value":"reverse-harem"},{"name":"Samurai","value":"samurai"},
{"name":"Vampire","value":"vampire"},{"name":"Mythology","value":"mythology"},{"name":"High Stakes Game","value":"high-stakes-game"},{"name":"Strategy Game","value":"strategy-game"},
{"name":"Magical Sex Shift","value":"magical-sex-shift"},{"name":"Racing","value":"racing"},{"name":"Isekai","value":"isekai"},{"name":"Workplace","value":"workplace"},{"name":"Iyashikei","value":"iyashikei"},
{"name":"Time Travel","value":"time-travel"},{"name":"Gore","value":"gore"},{"name":"Educational","value":"educational"},{"name":"Delinquents","value":"delinquents"},{"name":"Organized Crime","value":"organized-crime"},
{"name":"Otaku Culture","value":"otaku-culture"},{"name":"Medical","value":"medical"},{"name":"Survival","value":"survival"},{"name":"Reincarnation","value":"reincarnation"},{"name":"Showbiz","value":"showbiz"},
{"name":"Team Sports","value":"team-sports"},{"name":"Mahou Shoujo","value":"mahou-shoujo"},{"name":"Combat Sports","value":"combat-sports"},{"name":"Crossdressing","value":"crossdressing"},
{"name":"Visual Arts","value":"visual-arts"},{"name":"Childcare","value":"childcare"},{"name":"Pets","value":"pets"},{"name":"Love Status Quo","value":"love-status-quo"},{"name":"Urban Fantasy","value":"urban-fantasy"},
{"name":"Villainess","value":"villainess"}
],
"type":[
{"name":"TV","value":"tv"},{"name":"Movie","value":"movie"},{"name":"OVA","value":"ova"},{"name":"ONA","value":"ona"},{"name":"Special","value":"special"},{"name":"Music","value":"music"}
],
"demographic":[
{"name":"Shounen","value":"shounen"},{"name":"Shoujo","value":"shoujo"},{"name":"Seinen","value":"seinen"},{"name":"Kids","value":"kids"},{"name":"Josei","value":"josei"}
],
"status":[
{"value":"airing"},{"value":"completed"}
]
};
const filterDefaultRules = {
genre: {
include: "and",
exclude: "and"
},
theme: {
include: "and",
exclude: "and"
},
demographic: {
include: "or",
exclude: "and"
},
type: {
include: "or",
exclude: "and"
},
season: {
include: "or",
exclude: "and"
},
status: {
include: "or"
}
};
const filterRules = JSON.parse(JSON.stringify(filterDefaultRules));
function buildFilterString(type, value) {
if (type === 'status') return value;
if (type === 'season_entry') return `season/${getSeasonName(value.season)}-${value.year}`;
return type + '/' + value;
}
const seasonFilterRegex = /^!?(spring|summer|winter|fall)-(\d{4})\.\.(spring|summer|winter|fall)-(\d{4})$/;
function getFilteredList(filtersInput) {
let filtersChecked = 0;
let filtersTotal = 0;
function getPage(pageUrl) {
return new Promise((resolve, reject) => {
const cached = filterSearchCache[pageUrl];
if (cached !== undefined) { // If cache exists
if (cached === 'invalid') { // Not sure if it ever is 'invalid'
resolve([]);
return;
}
resolve(cached);
return;
}
const req = new XMLHttpRequest();
req.open('GET', pageUrl, true);
try {
req.send();
}
catch (err) {
console.error(err);
reject('A network error occured.');
return;
}
req.onload = () => {
if (req.status !== 200) {
filterSearchCache[pageUrl] = [];
resolve([]);
return;
}
const animeList = getAnimeList($(req.response));
filterSearchCache[pageUrl] = animeList;
resolve(animeList);
};
});
}
function getLists(filters) {
const lists = [];
return new Promise((resolve, reject) => {
function check() {
if (filters.length > 0) {
repeat(filters.shift());
}
else {
resolve(lists);
}
}
function repeat(filter) {
const filterType = filter.type;
if (filter.value === 'none') {
filtersTotal += filterValues[filterType].length;
getLists(filterValues[filterType].map(a => {return {type: filterType, value: a.value, exclude: false};})).then((filtered) => {
getPage('/anime').then((unfiltered) => {
const none = [];
for (const entry of unfiltered) {
const found = filtered.find(list => list.entries.find(a => a.name === entry.name));
if (!filter.exclude && found !== undefined) continue;
if (filter.exclude && found === undefined) continue;
none.push(entry);
}
lists.push({
type: filterType,
excludedFilter: false,
entries: none
});
check();
});
});
return;
}
if (filterType === 'season') {
const seasonFilters = getSeasonTimeframe(filter.value.from, filter.value.to);
filtersTotal += seasonFilters.length;
getLists(seasonFilters).then((filtered) => {
const filtersResult = [];
if (filter.exclude) getPage('/anime').then((unfiltered) => {
for (const entry of unfiltered) {
if (filtered.find(list => list.entries.find(a => a.name === entry.name)) !== undefined) continue;
filtersResult.push(entry);
}
lists.push({
type: 'season',
excludedFilter: true,
entries: filtersResult
});
check();
});
else {
for (const list of filtered) {
filtersResult.push(...list.entries);
}
lists.push({
type: 'season',
excludedFilter: false,
entries: filtersResult
});
check();
}
});
return;
}
if (filter.exclude) {
getPage('/anime/' + buildFilterString(filterType, filter.value)).then((filtered) => {
getPage('/anime').then((unfiltered) => {
const included = [];
for (const entry of unfiltered) {
if (filtered.find(a => a.name === entry.name) !== undefined) continue;
included.push(entry);
}
lists.push({
type: filterType,
excludedFilter: true,
entries: included
});
check();
});
});
return;
}
getPage('/anime/' + buildFilterString(filterType, filter.value)).then((result) => {
if (result !== undefined) {
lists.push({
type: filterType,
excludedFilter: false,
entries: result
});
}
if (filtersTotal > 0) {
filtersChecked++;
$($('.anitracker-filter-spinner>span')[0]).text(Math.floor((filtersChecked/filtersTotal) * 100).toString() + '%');
}
check();
});
}
check();
});
}
function combineLists(lists, rule) {
if (lists.length === 0) return [];
// Start with the first filter list result, then compare others to it
let combinedList = lists[0];
lists.splice(0,1); // Remove the first entry
for (const list of lists) {
// If the rule of this filter type is 'or,' start from the current list
// Otherwise, start from an empty list
const updatedList = rule === 'or' ? combinedList : [];
if (rule === 'and') for (const anime of list) {
// The anime has to exist in both the current and the checked list
if (combinedList.find(a => a.name === anime.name) === undefined) continue;
updatedList.push(anime);
}
else if (rule === 'or') for (const anime of list) {
// The anime just has to not already exist in the current list
if (combinedList.find(a => a.name === anime.name) !== undefined) continue;
updatedList.push(anime);
}
combinedList = updatedList;
}
return combinedList;
}
return new Promise((resolve, reject) => {
const filters = JSON.parse(JSON.stringify(filtersInput));
if (filters.length === 0) {
getPage('/anime').then((response) => {
if (response === undefined) {
alert('Page loading failed.');
reject('Anime index page not reachable.');
return;
}
resolve(response);
});
return;
}
filtersTotal = filters.length;
getLists(filters).then((listsInput) => {
const lists = JSON.parse(JSON.stringify(listsInput));
// groupedLists entries have the following format:
/* {
type, // the type of filter, eg. 'genre'
includeLists: [
<list of included anime>
],
excludeLists: [
<list of excluded anime>
]
}
*/
const groupedLists = [];
for (const list of lists) {
let foundGroup = groupedLists.find(a => a.type === list.type);
if (foundGroup === undefined) {
groupedLists.push({
type: list.type,
includeLists: [],
excludeLists: []
});
foundGroup = groupedLists[groupedLists.length - 1];
}
if (list.excludedFilter) foundGroup.excludeLists.push(list.entries);
else foundGroup.includeLists.push(list.entries);
}
let finalList;
for (const group of groupedLists) {
const includeList = combineLists(group.includeLists, filterRules[group.type].include);
const excludeList = combineLists(group.excludeLists, filterRules[group.type].exclude);
// Combine the include and exclude lists
// If the exclude list exists, start from an empty list
// Otherwise, just default to the include list
let groupFinalList = [];
if (excludeList.length > 0 && includeList.length > 0) {
const combineRule = filterRules[group.type].combined;
for (const entry of excludeList) {
if (groupFinalList.find(a => a.name === entry.name) !== undefined) continue; // Don't include duplicates
if (combineRule === 'or') {
if (includeList.find(a => a.name === entry.name) !== undefined) continue;
groupFinalList.push(entry);
continue;
}
// Otherwise, the rule is 'and'
if (includeList.find(a => a.name === entry.name) === undefined) continue;
groupFinalList.push(entry);
}
}
else if (excludeList.length === 0) groupFinalList = includeList;
else if (includeList.length === 0) groupFinalList = excludeList;
// If the current final list is undefined, just add the resulting list to it and continue
if (finalList === undefined) {
finalList = groupFinalList;
continue;
}
const newFinalList = [];
// Loop through the resulting list
// Join together with 'and'
for (const anime of groupFinalList) {
if (finalList.find(a => a.name === anime.name) === undefined) continue;
newFinalList.push(anime);
}
finalList = newFinalList;
}
resolve(finalList);
});
});
}
function searchList(fuseClass, list, query, limit = 80) {
const fuse = new fuseClass(list, {
keys: ['name'],
findAllMatches: true
});
const matching = fuse.search(query);
return matching.map(a => {return a.item}).splice(0,limit);
}
function timeSince(date) {
const seconds = Math.floor((new Date() - date) / 1000);
let interval = Math.floor(seconds / 31536000);
if (interval >= 1) {
return interval + " year" + (interval > 1 ? 's' : '');
}
interval = Math.floor(seconds / 2592000);
if (interval >= 1) {
return interval + " month" + (interval > 1 ? 's' : '');
}
interval = Math.floor(seconds / 86400);
if (interval >= 1) {
return interval + " day" + (interval > 1 ? 's' : '');
}
interval = Math.floor(seconds / 3600);
if (interval >= 1) {
return interval + " hour" + (interval > 1 ? 's' : '');
}
interval = Math.floor(seconds / 60);
if (interval >= 1) {
return interval + " minute" + (interval > 1 ? 's' : '');
}
return seconds + " second" + (seconds > 1 ? 's' : '');
}
if (window.location.pathname.startsWith('/customlink')) {
const parts = {
animeSession: '',
episodeSession: '',
time: -1
};
const entries = Array.from(new URLSearchParams(window.location.search).entries()).sort((a,b) => a[0] > b[0] ? 1 : -1);
for (const entry of entries) {
if (entry[0] === 'a') {
parts.animeSession = getAnimeData(decodeURIComponent(entry[1])).session;
continue;
}
if (entry[0] === 'e') {
if (parts.animeSession === '') return;
parts.episodeSession = getEpisodeSession(parts.animeSession, +entry[1]);
continue;
}
if (entry[0] === 't') {
if (parts.animeSession === '') return;
if (parts.episodeSession === '') continue;
parts.time = +entry[1];
continue;
}
}
const destination = (() => {
if (parts.animeSession !== '' && parts.episodeSession === '' && parts.time === -1) {
return '/anime/' + parts.animeSession + '?ref=customlink';
}
if (parts.animeSession !== '' && parts.episodeSession !== '' && parts.time === -1) {
return '/play/' + parts.animeSession + '/' + parts.episodeSession + '?ref=customlink';
}
if (parts.animeSession !== '' && parts.episodeSession !== '' && parts.time >= 0) {
return '/play/' + parts.animeSession + '/' + parts.episodeSession + '?time=' + parts.time + '&ref=customlink';
}
return undefined;
})();
if (destination !== undefined) {
document.title = "Redirecting... :: animepahe";
$('h1').text('Redirecting...');
window.location.replace(destination);
}
return;
}
// Main key events
if (!is404) $(document).on('keydown', (e) => {
if ($(e.target).is(':input')) return;
if (modalIsOpen() && ['Escape','Backspace'].includes(e.key)) {
modalCloseFunction();
return;
}
if (!isEpisode() || modalIsOpen()) return;
if (e.key === 't') {
toggleTheatreMode();
}
else {
sendMessage({action:"key",key:e.key});
$('.embed-responsive-item')[0].contentWindow.focus();
if ([" "].includes(e.key)) e.preventDefault();
}
});
if (window.location.pathname.startsWith('/queue')) {
$(`
<span style="font-size:.6em;">&nbsp;&nbsp;&nbsp;(Incoming episodes)</span>
`).appendTo('h2');
}
if (/^\/anime\/\w+(\/[\w\-\.]+)?$/.test(window.location.pathname)) {
if (is404) return;
const filter = /\/anime\/([^\/]+)\/?([^\/]+)?/.exec(window.location.pathname);
if (filter[2] !== undefined) {
if (filterRules[filter[1]] === undefined) return;
if (filter[1] === 'season') {
window.location.replace(`/anime?${filter[1]}=${filter[2]}..${filter[2]}`);
return;
}
window.location.replace(`/anime?${filter[1]}=${filter[2]}`);
}
else {
window.location.replace(`/anime?other=${filter[1]}`);
}
return;
}
function getDayName(day) {
return [
"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"
][day];
}
function toHtmlCodes(string) {
return $('<div>').text(string).html().replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
// Bookmark & episode feed header buttons
$(`
<div class="anitracker-header">
<button class="anitracker-header-notifications anitracker-header-button" title="View episode feed">
<i class="fa fa-bell" aria-hidden="true"></i>
<i style="display:none;" aria-hidden="true" class="fa fa-circle anitracker-header-notifications-circle"></i>
</button>
<button class="anitracker-header-bookmark anitracker-header-button" title="View bookmarks"><i class="fa fa-bookmark" aria-hidden="true"></i></button>
</div>`).insertAfter('.navbar-nav');
let currentNotificationIndex = 0;
function openNotificationsModal() {
currentNotificationIndex = 0;
const oldStorage = getStorage();
$('#anitracker-modal-body').empty();
$(`
<h4>Episode Feed</h4>
<div class="btn-group" style="margin-bottom: 10px;">
<button class="btn btn-secondary anitracker-view-notif-animes">
Handle Feed...
</button>
</div>
<div class="anitracker-modal-list-container">
<div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;">
<div id="anitracker-notifications-list-spinner" style="display:flex;justify-content:center;">
<div class="spinner-border text-danger" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
</div>`).appendTo('#anitracker-modal-body');
$('.anitracker-view-notif-animes').on('click', () => {
$('#anitracker-modal-body').empty();
const storage = getStorage();
$(`
<h4>Handle Episode Feed</h4>
<div class="anitracker-modal-list-container">
<div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;"></div>
</div>
`).appendTo('#anitracker-modal-body');
[...storage.notifications.anime].sort((a,b) => a.latest_episode > b.latest_episode ? 1 : -1).forEach(g => {
const latestEp = new Date(g.latest_episode + " UTC");
const latestEpString = g.latest_episode !== undefined ? `${getDayName(latestEp.getDay())} ${latestEp.toLocaleTimeString()}` : "None found";
$(`
<div class="anitracker-modal-list-entry" animeid="${g.id}" animename="${toHtmlCodes(g.name)}">
<a href="/a/${g.id}" target="_blank" title="${toHtmlCodes(g.name)}">
${g.name}
</a><br>
<span>
Latest episode: ${latestEpString}
</span><br>
<div class="btn-group">
<button class="btn btn-danger anitracker-delete-button anitracker-flat-button" title="Remove this anime from the episode feed">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Remove
</button>
</div>
<div class="btn-group">
<button class="btn btn-secondary anitracker-get-all-button anitracker-flat-button" title="Put all episodes in the feed" ${g.hasFirstEpisode ? 'disabled=""' : ''}>
<i class="fa fa-rotate-right" aria-hidden="true"></i>
&nbsp;Get All
</button>
</div>
</div>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
});
if (storage.notifications.anime.length === 0) {
$("<span>Use the <i class=\"fa fa-bell\" title=\"bell\"></i> button on an ongoing anime to add it to the feed.</span>").appendTo('#anitracker-modal-body .anitracker-modal-list');
}
$('.anitracker-modal-list-entry .anitracker-get-all-button').on('click', (e) => {
const elem = $(e.currentTarget);
const id = +elem.parents().eq(1).attr('animeid');
const storage = getStorage();
const found = storage.notifications.anime.find(a => a.id === id);
if (found === undefined) {
console.error("[AnimePahe Improvements] Couldn't find feed for anime with id " + id);
return;
}
found.hasFirstEpisode = true;
found.updateFrom = 0;
saveData(storage);
elem.replaceClass("btn-secondary", "btn-primary");
setTimeout(() => {
elem.replaceClass("btn-primary", "btn-secondary");
elem.prop('disabled', true);
}, 200);
});
$('.anitracker-modal-list-entry .anitracker-delete-button').on('click', (e) => {
const parent = $(e.currentTarget).parents().eq(1);
const name = parent.attr('animename');
toggleNotifications(name, +parent.attr('animeid'));
const name2 = getAnimeName();
if (name2.length > 0 && name2 === name) {
$('.anitracker-notifications-toggle .anitracker-title-icon-check').hide();
}
parent.remove();
});
openModal(openNotificationsModal);
});
const animeData = [];
const queue = [...oldStorage.notifications.anime];
openModal().then(() => {
if (queue.length > 0) next();
else done();
});
async function next() {
if (queue.length === 0) done();
const anime = queue.shift();
const data = await updateNotifications(anime.name);
if (data === -1) {
$("<span>An error occured.</span>").appendTo('#anitracker-modal-body .anitracker-modal-list');
return;
}
animeData.push({
id: anime.id,
data: data
});
if (queue.length > 0 && $('#anitracker-notifications-list-spinner').length > 0) next();
else done();
}
function done() {
if ($('#anitracker-notifications-list-spinner').length === 0) return;
const storage = getStorage();
let removedAnime = 0;
for (const anime of storage.notifications.anime) {
if (anime.latest_episode === undefined || anime.dont_ask === true) continue;
const time = Date.now() - new Date(anime.latest_episode + " UTC").getTime();
if ((time / 1000 / 60 / 60 / 24 / 7) > 2) {
const remove = confirm(`[AnimePahe Improvements]\n\nThe latest episode for ${anime.name} was more than 2 weeks ago. Remove it from the feed?\n\nThis prompt will not be shown again.`);
if (remove === true) {
toggleNotifications(anime.name, anime.id);
removedAnime++;
}
else {
anime.dont_ask = true;
saveData(storage);
}
}
}
if (removedAnime > 0) {
openNotificationsModal();
return;
}
$('#anitracker-notifications-list-spinner').remove();
storage.notifications.episodes.sort((a,b) => a.time < b.time ? 1 : -1);
storage.notifications.lastUpdated = Date.now();
saveData(storage);
if (storage.notifications.episodes.length === 0) {
$("<span>Nothing here yet!</span>").appendTo('#anitracker-modal-body .anitracker-modal-list');
}
else addToList(20);
}
function addToList(num) {
const storage = getStorage();
const index = currentNotificationIndex;
for (let i = currentNotificationIndex; i < storage.notifications.episodes.length; i++) {
const ep = storage.notifications.episodes[i];
if (ep === undefined) break;
currentNotificationIndex++;
const data = animeData.find(a => a.id === ep.animeId)?.data;
if (data === undefined) {
console.error(`[AnimePahe Improvements] Could not find corresponding anime "${ep.animeName}" with ID ${ep.animeId} (episode ${ep.episode})`);
continue;
}
const releaseTime = new Date(ep.time + " UTC");
$(`
<div class="anitracker-big-list-item anitracker-notification-item${ep.watched ? "" : " anitracker-notification-item-unwatched"} anitracker-temp" anime-data="${data.id}" episode-data="${ep.episode}">
<a href="/play/${data.session}/${ep.session}" target="_blank" title="${toHtmlCodes(data.title)}">
<img src="${data.poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" alt="[Thumbnail of ${toHtmlCodes(data.title)}]"}>
<i class="fa ${ep.watched ? 'fa-eye-slash' : 'fa-eye'} anitracker-watched-toggle" tabindex="0" aria-hidden="true" title="Mark this episode as ${ep.watched ? 'unwatched' : 'watched'}"></i>
<div class="anitracker-main-text">${data.title}</div>
<div class="anitracker-subtext"><strong>Episode ${ep.episode}</strong></div>
<div class="anitracker-subtext">${timeSince(releaseTime)} ago (${releaseTime.toLocaleDateString()})</div>
</a>
</div>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
if (i > index+num-1) break;
}
$('.anitracker-notification-item.anitracker-temp').on('click', (e) => {
$(e.currentTarget).find('a').blur();
});
$('.anitracker-notification-item.anitracker-temp .anitracker-watched-toggle').on('click keydown', (e) => {
if (e.type === 'keydown' && e.key !== "Enter") return;
e.preventDefault();
const storage = getStorage();
const elem = $(e.currentTarget);
const parent = elem.parents().eq(1);
const ep = storage.notifications.episodes.find(a => a.animeId === +parent.attr('anime-data') && a.episode === +parent.attr('episode-data'));
if (ep === undefined) {
console.error("[AnimePahe Improvements] couldn't mark episode as watched/unwatched");
return;
}
parent.toggleClass('anitracker-notification-item-unwatched');
elem.toggleClass('fa-eye').toggleClass('fa-eye-slash');
if (e.type === 'click') elem.blur();
ep.watched = !ep.watched;
elem.attr('title', `Mark this episode as ${ep.watched ? 'unwatched' : 'watched'}`);
saveData(storage);
});
$('.anitracker-notification-item.anitracker-temp').removeClass('anitracker-temp');
}
$('#anitracker-modal-body').on('scroll', () => {
const elem = $('#anitracker-modal-body');
if (elem.scrollTop() >= elem[0].scrollTopMax) {
if ($('.anitracker-view-notif-animes').length === 0) return;
addToList(20);
}
});
}
$('.anitracker-header-notifications').on('click', openNotificationsModal);
$('.anitracker-header-bookmark').on('click', () => {
$('#anitracker-modal-body').empty();
const storage = getStorage();
$(`
<h4>Bookmarks</h4>
<div class="anitracker-modal-list-container">
<div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;">
<div class="btn-group">
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-modal-search" placeholder="Search">
<button dir="down" class="btn btn-secondary dropdown-toggle anitracker-reverse-order-button anitracker-list-btn" title="Sort direction (down is default, and means newest first)"></button>
</div>
</div>
</div>
`).appendTo('#anitracker-modal-body');
$('.anitracker-modal-search').on('input', (e) => {
setTimeout(() => {
const query = $(e.target).val();
for (const entry of $('.anitracker-modal-list-entry')) {
if ($($(entry).find('a,span')[0]).text().toLowerCase().includes(query)) {
$(entry).show();
continue;
}
$(entry).hide();
}
}, 10);
});
function applyDeleteEvents() {
$('.anitracker-modal-list-entry button').on('click', (e) => {
const id = $(e.currentTarget).parent().attr('animeid');
toggleBookmark(id);
const data = getAnimeData();
if (data !== undefined && data.id === +id) {
$('.anitracker-bookmark-toggle .anitracker-title-icon-check').hide();
}
$(e.currentTarget).parent().remove();
});
}
// When clicking the reverse order button
$('.anitracker-reverse-order-button').on('click', (e) => {
const btn = $(e.target);
if (btn.attr('dir') === 'down') {
btn.attr('dir', 'up');
btn.addClass('anitracker-up');
}
else {
btn.attr('dir', 'down');
btn.removeClass('anitracker-up');
}
const entries = [];
for (const entry of $('.anitracker-modal-list-entry')) {
entries.push(entry.outerHTML);
}
entries.reverse();
$('.anitracker-modal-list-entry').remove();
for (const entry of entries) {
$(entry).appendTo($('.anitracker-modal-list'));
}
applyDeleteEvents();
});
[...storage.bookmarks].reverse().forEach(g => {
$(`
<div class="anitracker-modal-list-entry" animeid="${g.id}">
<a href="/a/${g.id}" target="_blank" title="${toHtmlCodes(g.name)}">
${g.name}
</a><br>
<button class="btn btn-danger anitracker-flat-button" title="Remove this bookmark">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Remove
</button>
</div>`).appendTo('#anitracker-modal-body .anitracker-modal-list')
});
if (storage.bookmarks.length === 0) {
$(`<span style="display: block;">No bookmarks yet!</span>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
}
applyDeleteEvents();
openModal();
$('#anitracker-modal-body')[0].scrollTop = 0;
});
function toggleBookmark(id, name=undefined) {
const storage = getStorage();
const found = storage.bookmarks.find(g => g.id === +id);
if (found !== undefined) {
const index = storage.bookmarks.indexOf(found);
storage.bookmarks.splice(index, 1);
saveData(storage);
return false;
}
if (name === undefined) return false;
storage.bookmarks.push({
id: +id,
name: name
});
saveData(storage);
return true;
}
function toggleNotifications(name, id = undefined) {
const storage = getStorage();
const found = (() => {
if (id !== undefined) return storage.notifications.anime.find(g => g.id === id);
else return storage.notifications.anime.find(g => g.name === name);
})();
if (found !== undefined) {
const index = storage.notifications.anime.indexOf(found);
storage.notifications.anime.splice(index, 1);
storage.notifications.episodes = storage.notifications.episodes.filter(a => a.animeName !== found.name); // Uses the name, because old data might not be updated to use IDs
saveData(storage);
return false;
}
const animeData = getAnimeData(name);
storage.notifications.anime.push({
name: name,
id: animeData.id
});
saveData(storage);
return true;
}
async function updateNotifications(animeName, storage = getStorage()) {
const nobj = storage.notifications.anime.find(g => g.name === animeName);
if (nobj === undefined) {
toggleNotifications(animeName);
return;
}
const data = await asyncGetAnimeData(animeName, nobj.id);
if (data === undefined) return -1;
const episodes = await asyncGetAllEpisodes(data.session, 'desc');
if (episodes === undefined) return 0;
return new Promise((resolve, reject) => {
if (episodes.length === 0) resolve(undefined);
nobj.latest_episode = episodes[0].created_at;
if (nobj.name !== data.title) {
for (const ep of storage.notifications.episodes) {
if (ep.animeName !== nobj.name) continue;
ep.animeName = data.title;
}
nobj.name = data.title;
}
const compareUpdateTime = nobj.updateFrom ?? storage.notifications.lastUpdated;
if (nobj.updateFrom !== undefined) delete nobj.updateFrom;
for (const ep of episodes) {
const found = storage.notifications.episodes.find(a => a.episode === ep.episode && a.animeId === nobj.id) ?? storage.notifications.episodes.find(a => a.episode === ep.episode && a.animeName === data.title);
if (found !== undefined) {
found.session = ep.session;
if (found.animeId === undefined) found.animeId = nobj.id;
if (episodes.indexOf(ep) === episodes.length - 1) nobj.hasFirstEpisode = true;
continue;
}
if (new Date(ep.created_at + " UTC").getTime() < compareUpdateTime) {
continue;
}
storage.notifications.episodes.push({
animeName: nobj.name,
animeId: nobj.id,
session: ep.session,
episode: ep.episode,
time: ep.created_at,
watched: false
});
}
const length = storage.notifications.episodes.length;
if (length > 100) {
storage.notifications.episodes = storage.notifications.episodes.slice(length - 100);
}
saveData(storage);
resolve(data);
});
}
const paramArray = Array.from(new URLSearchParams(window.location.search));
const refArg01 = paramArray.find(a => a[0] === 'ref');
if (refArg01 !== undefined) {
const ref = refArg01[1];
if (ref === '404') {
alert('[AnimePahe Improvements]\n\nThe session was outdated, and has been refreshed. Please try that link again.');
}
else if (ref === 'customlink' && isEpisode() && initialStorage.settings.autoDelete) {
const name = getAnimeName();
const num = getEpisodeNum();
if (initialStorage.linkList.find(e => e.animeName === name && e.type === 'episode' && e.episodeNum !== num)) { // If another episode is already stored
$(`
<span style="display:block;width:100%;text-align:center;" class="anitracker-from-share-warning">
The current episode data for this anime was not replaced due to coming from a share link.
<br>Refresh this page to replace it.
<br><span class="anitracker-text-button" tabindex="0">Dismiss</span>
</span>`).prependTo('.content-wrapper');
$('.anitracker-from-share-warning>span').on('click keydown', function(e) {
if (e.type === 'keydown' && e.key !== "Enter") return;
$(e.target).parent().remove();
});
}
}
window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
}
function getCurrentSeason() {
const month = new Date().getMonth();
return Math.trunc(month/3);
}
function getFiltersFromParams(params) {
const filters = [];
for (const [key, value] of params.entries()) {
const inputFilters = value.split(','); // Get all filters of this filter type
for (const filter of inputFilters) {
if (filterRules[key] === undefined) continue;
const exclude = filter.startsWith('!');
if (key === 'season' && seasonFilterRegex.test(filter)) {
const parts = seasonFilterRegex.exec(filter);
if (!parts.includes(undefined) && ![parseInt(parts[2]),parseInt(parts[4])].includes(NaN)) {
filters.push({
type: 'season',
value: {
from: { season: getSeasonValue(parts[1]), year: parseInt(parts[2]) },
to: { season: getSeasonValue(parts[3]), year: parseInt(parts[4]) }
},
exclude: exclude
});
}
continue;
}
filters.push({
type: key,
value: filter.replace(/^!/,''),
exclude: exclude
});
}
}
return filters;
}
function loadIndexPage() {
const animeList = getAnimeList();
filterSearchCache['/anime'] = JSON.parse(JSON.stringify(animeList));
$(`
<div id="anitracker" class="anitracker-index" style="margin-bottom: 10px;">
<div class="anitracker-filter-input" data-filter-type="genre">
<button class="anitracker-filter-rules" title="Change filter logic" data-filter-type="genre"><i class="fa fa-sliders"></i></button>
<div>
<div data-filter-type="genre" class="anitracker-applied-filters"></div><span data-filter-type="genre" role="textbox" contenteditable="" spellcheck="false" class="anitracker-text-input"></span>
</div>
</div>
<div class="anitracker-filter-input" data-filter-type="theme">
<button class="anitracker-filter-rules" title="Change filter logic" data-filter-type="theme"><i class="fa fa-sliders"></i></button>
<div>
<div data-filter-type="theme" class="anitracker-applied-filters"></div><span data-filter-type="theme" role="textbox" contenteditable="" spellcheck="false" class="anitracker-text-input"></span>
</div>
</div>
<div class="anitracker-filter-input" data-filter-type="type">
<button class="anitracker-filter-rules" title="Change filter logic" data-filter-type="type"><i class="fa fa-sliders"></i></button>
<div>
<div data-filter-type="type" class="anitracker-applied-filters"></div><span data-filter-type="type" role="textbox" contenteditable="" spellcheck="false" class="anitracker-text-input"></span>
</div>
</div>
<div class="anitracker-filter-input" data-filter-type="demographic">
<button class="anitracker-filter-rules" title="Change filter logic" data-filter-type="demographic"><i class="fa fa-sliders"></i></button>
<div>
<div data-filter-type="demographic" class="anitracker-applied-filters"></div><span data-filter-type="demographic" role="textbox" contenteditable="" spellcheck="false" class="anitracker-text-input"></span>
</div>
</div>
<div style="margin-left: auto;">
<div class="btn-group">
<button class="btn dropdown-toggle btn-dark" id="anitracker-status-button" data-bs-toggle="dropdown" data-toggle="dropdown" title="Choose status">All</button>
</div>
<div class="btn-group">
<button class="btn btn-dark" id="anitracker-time-search-button" title="Set season filter">
<svg fill="#ffffff" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve" aria-hidden="true">
<path d="M256,0C114.842,0,0,114.842,0,256s114.842,256,256,256s256-114.842,256-256S397.158,0,256,0z M374.821,283.546H256 c-15.148,0-27.429-12.283-27.429-27.429V137.295c0-15.148,12.281-27.429,27.429-27.429s27.429,12.281,27.429,27.429v91.394h91.392 c15.148,0,27.429,12.279,27.429,27.429C402.249,271.263,389.968,283.546,374.821,283.546z"/>
</svg>
</button>
</div>
</div>
<div id="anitracker-filter-dropdown-container"></div>
</div>
<div id="anitracker-row-2">
<span style="font-size: 1.2em;color:#ddd;" id="anitracker-filter-result-count">Filter results: <span>${animeList.length}</span></span>
<div style="float: right; margin-right: 6px; margin-bottom: 2rem;">
<div class="btn-group">
<button class="btn btn-dark" id="anitracker-apply-filters" title="Apply selected filters"><i class="fa fa-check" aria-hidden="true"></i>&nbsp;&nbsp;Apply</button>
</div>
<div class="btn-group">
<input id="anitracker-anime-list-search" disabled="" autocomplete="off" class="form-control anitracker-text-input-bar" style="width: 150px;" placeholder="Loading...">
</div>
<div class="btn-group">
<button class="btn btn-dark" id="anitracker-random-anime" title="Open a random anime from within the selected filters">
<i class="fa fa-random" aria-hidden="true"></i>
&nbsp;Random Anime
</button>
</div>
</div>
</div>`).insertBefore('.index');
function getDropdownButtons(filters, type) {
return filters.sort((a,b) => a.name > b.name ? 1 : -1).concat({value: 'none', name: '(None)'}).map(g => $(`<button data-filter-type="${type}" data-filter-value="${g.value}">${g.name}</button>`));
}
$(`<div id="anitracker-genre-dropdown" tabindex="-1" data-filter-type="genre" class="dropdown-menu anitracker-dropdown-content anitracker-filter-dropdown">`).appendTo('#anitracker-filter-dropdown-container');
getDropdownButtons(filterValues.genre, 'genre').forEach(g => { g.appendTo('#anitracker-genre-dropdown') });
$(`<div id="anitracker-theme-dropdown" tabindex="-1" data-filter-type="theme" class="dropdown-menu anitracker-dropdown-content anitracker-filter-dropdown">`).appendTo('#anitracker-filter-dropdown-container');
getDropdownButtons(filterValues.theme, 'theme').forEach(g => { g.appendTo('#anitracker-theme-dropdown') });
$(`<div id="anitracker-type-dropdown" tabindex="-1" data-filter-type="type" class="dropdown-menu anitracker-dropdown-content anitracker-filter-dropdown">`).appendTo('#anitracker-filter-dropdown-container');
getDropdownButtons(filterValues.type, 'type').forEach(g => { g.appendTo('#anitracker-type-dropdown') });
$(`<div id="anitracker-demographic-dropdown" tabindex="-1" data-filter-type="demographic" class="dropdown-menu anitracker-dropdown-content anitracker-filter-dropdown">`).appendTo('#anitracker-filter-dropdown-container');
getDropdownButtons(filterValues.demographic, 'demographic').forEach(g => { g.appendTo('#anitracker-demographic-dropdown') });
$(`<div id="anitracker-status-dropdown" tabindex="-1" data-filter-type="status" class="dropdown-menu anitracker-dropdown-content anitracker-filter-dropdown special">`).insertAfter('#anitracker-status-button');
['all','airing','completed'].forEach(g => { $(`<button data-filter-type="status" data-filter-value="${g}">${g[0].toUpperCase() + g.slice(1)}</button>`).appendTo('#anitracker-status-dropdown') });
$(`<button data-filter-type="status" data-filter-value="none">(No status)</button>`).appendTo('#anitracker-status-dropdown');
const timeframeSettings = {
enabled: false
};
const placeholderTexts = {
'genre': 'Genre',
'theme': 'Theme',
'type': 'Type',
'demographic': 'Demographic'
}
const selectedFilters = [];
const appliedFilters = [];
function getElemsFromFilterType(filterType) {
const elems = {};
if (filterType === undefined) return elems;
for (const inp of $('.anitracker-filter-input')) {
if ($(inp).data('filter-type') !== filterType) continue;
elems.parent = $(inp);
elems.filterIcons = Array.from($(inp).find('.anitracker-filter-icon'));
elems.filterIconContainer = $(inp).find('.anitracker-applied-filters');
elems.input = $(inp).find('.anitracker-text-input');
elems.inputPlaceholder = $(inp).find('.anitracker-placeholder');
elems.scrollingDiv = $(inp).find('>div');
elems.filterRuleButton = $(inp).find('.anitracker-filter-rules');
break;
}
for (const drop of $('.anitracker-filter-dropdown')) {
if ($(drop).data('filter-type') !== filterType) continue;
elems.dropdown = $(drop);
}
return elems;
}
function getFilterDataFromElem(jquery) {
return {
type: jquery.data('filter-type'),
value: jquery.data('filter-value'),
exclude: jquery.data('filter-exclude') === true
}
}
function getInputText(elem) {
return elem.contents().filter(function() {
return this.nodeType === Node.TEXT_NODE;
}).text().trim();
}
function clearPlaceholder(elem) {
elem.find('.anitracker-placeholder').remove();
}
function addPlaceholder(elem, filterType) {
if (getInputText(elem) !== '' || elem.find('.anitracker-placeholder').length > 0) return;
$(`<span data-filter-type="${filterType}" class="anitracker-placeholder">${placeholderTexts[filterType]}</span>`).prependTo(elem);
}
function setChangesToApply(on) {
const elem = $('#anitracker-apply-filters');
if (on) elem.addClass('btn-primary').removeClass('btn-dark');
else elem.removeClass('btn-primary').addClass('btn-dark');
}
function updateApplyButton() {
setChangesToApply(JSON.stringify(selectedFilters) !== JSON.stringify(appliedFilters));
}
function showDropdown(elem, parentElem) {
for (const type of Object.keys(filterRules)) {
const elems = getElemsFromFilterType(type);
if (elems.dropdown === undefined || elems.dropdown.length === 0 || elems.dropdown.hasClass('special')) continue;
elems.dropdown.hide();
}
const top = $(parentElem).position().top + $(parentElem).outerHeight(true);
const left = $(parentElem).position().left;
elem.css('top',top).css('left',left);
elem.show();
elem.scrollTop(0);
}
function checkCloseDropdown(elems) {
setTimeout(() => {
if (elems.dropdown.is(':focus,:focus-within') || elems.input.is(':focus')) return;
elems.dropdown.hide();
}, 1);
}
function fixSelection(elem) {
const sel = window.getSelection();
if (!$(sel.anchorNode).is('div')) return;
setSelection(elem);
}
function setSelection(elem) {
const sel = window.getSelection();
elem.focus();
const index = elem.text().length - 1 - elem.find('.anitracker-placeholder').text().length - 1;
const range = document.createRange();
range.setStart(elem[0], index > 0 ? index : 0);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
function scrollToBottom(elem) {
elem.scrollTop(9999);
}
['genre','theme','type','demographic'].forEach((type) => {
const elems = getElemsFromFilterType(type);
addPlaceholder(elems.input, type);
elems.input.css('width','100%').css('height','100%');
});
function getActiveFilter(filter) {
return selectedFilters.find(f => f.type === filter.type && f.value === filter.value && f.exclude === filter.exclude);
}
function refreshIconSymbol(elem) {
const excluded = elem.data('filter-exclude');
elem.find('i').remove();
if (excluded === undefined) return;
$(`<i class="fa fa-${excluded ? 'minus' : 'plus'}"></i>`).prependTo(elem);
}
function setStatusFilter(filter) {
for (const filter of selectedFilters.filter(f => f.type === 'status')) {
selectedFilters.splice(selectedFilters.indexOf(filter), 1);
}
for (const btn of $('#anitracker-status-dropdown>button')) {
const elem = $(btn);
const filterValue = elem.data('filter-value')
if (filterValue !== filter.value) {
elem.removeClass('anitracker-active');
continue;
}
$('#anitracker-status-button').text(elem.text());
if (filterValue !== 'all') elem.addClass('anitracker-active');
}
if (filter.value !== 'all') selectedFilters.push(filter);
if (filter.value === 'all') $('#anitracker-status-button').removeClass('anitracker-active');
else $('#anitracker-status-button').addClass('anitracker-active');
}
function addFilter(filter) {
if (filter.type === 'season') {
addSeasonFilter(filter);
return;
}
if (filter.type === 'status') {
setStatusFilter(filter);
return;
}
const elems = getElemsFromFilterType(filter.type);
elems.parent?.addClass('active');
elems.input?.css('width','').css('height','');
if (elems.input !== undefined) clearPlaceholder(elems.input);
if (getActiveFilter(filter) !== undefined || filterValues[filter.type] === undefined) return;
const filterEntry = filterValues[filter.type].find(f => f.value === filter.value);
const name = (() => {
if (filter.value === 'none') return '(None)';
else return filterEntry !== undefined ? filterEntry.name : filter.value;
})();
const icon = $(`<span class="anitracker-filter-icon ${filter.exclude ? 'excluded' : 'included'}" data-filter-type="${filter.type}" data-filter-value="${filter.value}" data-filter-exclude="${filter.exclude}">${name}</span>`).appendTo(elems.filterIconContainer);
refreshIconSymbol(icon);
icon.on('click', (e) => {
cycleFilter(getFilterDataFromElem($(e.currentTarget)));
});
for (const btn of elems.dropdown.find('button')) {
const elem = $(btn);
if (elem.data('filter-value') !== filter.value) continue;
if (filter.exclude !== undefined) elem.data('filter-exclude', filter.exclude);
if (filter.exclude) elem.addClass('excluded').removeClass('included');
else elem.addClass('included').removeClass('excluded');
}
if (filter.exclude === undefined) filter.exclude = false;
selectedFilters.push(filter);
}
function removeFilter(filter) {
const elems = getElemsFromFilterType(filter.type);
const activeFilter = getActiveFilter(filter);
if (activeFilter === undefined) return;
for (const icon of elems.filterIcons) {
const elem = $(icon);
if (elem.data('filter-value') !== filter.value) continue;
elem.remove();
}
for (const btn of elems.dropdown.find('button')) {
const elem = $(btn);
if (elem.data('filter-value') !== filter.value) continue;
elem.data('filter-exclude', '');
elem.removeClass('excluded').removeClass('included');
}
selectedFilters.splice(selectedFilters.indexOf(activeFilter), 1);
// Count remaining filters of the same type
const remainingFilters = selectedFilters.filter(f => f.type === filter.type);
if (remainingFilters.length === 0) {
elems.parent?.removeClass('active');
elems.input?.css('width','100%').css('height','100%');
if (elems.input !== undefined) addPlaceholder(elems.input, filter.type);
}
}
// Sets the filter to negative, doesn't actually invert it
function invertFilter(filter) {
const elems = getElemsFromFilterType(filter.type);
const activeFilter = getActiveFilter(filter);
if (activeFilter === undefined) return;
for (const icon of elems.filterIcons) {
const elem = $(icon);
if (elem.data('filter-value') !== filter.value) continue;
elem.removeClass('included').addClass('excluded');
elem.data('filter-exclude', true);
refreshIconSymbol(elem);
}
for (const btn of elems.dropdown.find('button')) {
const elem = $(btn);
if (elem.data('filter-value') !== filter.value) continue;
elem.removeClass('included').addClass('excluded');
elem.data('filter-exclude', true);
}
activeFilter.exclude = true;
}
function cycleFilter(filter) {
if (getActiveFilter(filter) === undefined) addFilter(filter);
else if (filter.exclude === false) invertFilter(filter);
else if (filter.exclude === true) removeFilter(filter);
updateApplyButton();
}
function removeSeasonFilters() {
for (const filter of selectedFilters.filter(f => f.type === 'season')) {
selectedFilters.splice(selectedFilters.indexOf(filter), 1);
}
}
function addSeasonFilter(filter) {
$('#anitracker-time-search-button').addClass('anitracker-active');
timeframeSettings.enabled = true;
timeframeSettings.inverted = filter.exclude === true;
timeframeSettings.from = filter.value.from;
timeframeSettings.to = filter.value.to;
selectedFilters.push(filter);
}
const searchParams = new URLSearchParams(window.location.search);
function setSearchParam(name, value) {
if (value === undefined) searchParams.delete(name);
else searchParams.set(name,value);
}
function getSearchParamsString(params) {
if (Array.from(params.entries()).length === 0) return '';
return '?' + decodeURIComponent(params.toString());
}
function updateSearchParams() {
window.history.replaceState({}, document.title, "/anime" + getSearchParamsString(searchParams));
}
function layoutTabless(entries) { // Tabless = without tabs
$('.index>').hide();
$('#anitracker-search-results').remove();
$(`<div class="row" id="anitracker-search-results"></div>`).prependTo('.index');
let elements = entries.map(match => {
return `
<div class="col-12 col-md-6">
${match.html}
</div>`;
});
if (entries.length === 0) elements = `<div class="col-12 col-md-6">No results found.</div>`;
Array.from($(elements)).forEach(a => {$(a).appendTo('#anitracker-search-results');});
}
function layoutAnime(entries) {
$('#anitracker-filter-result-count>span').text(entries.length);
const tabs = $('.tab-content>div');
tabs.find('.col-12').remove();
$('.nav-link').show();
$('.index>').show();
$('#anitracker-search-results').remove();
const sortedEntries = entries.sort((a,b) => a.name > b.name ? 1 : -1);
if (entries.length < 100) {
layoutTabless(sortedEntries);
$('#anitracker-anime-list-search').trigger('anitracker:search');
return;
}
for (const tab of tabs) {
const id = $(tab).attr('id');
const symbol = id.toLowerCase();
const matchingAnime = (() => {
if (symbol === 'hash') {
return sortedEntries.filter(a => /^(?![A-Za-z])./.test(a.name.toLowerCase()));
}
else return sortedEntries.filter(a => a.name.toLowerCase().startsWith(symbol));
})();
if (matchingAnime.length === 0) {
$(`.index .nav-link[href="#${id}"]`).hide();
continue;
}
const row = $(tab).find('.row');
for (const anime of matchingAnime) {
$(`<div class="col-12 col-md-6">
${anime.html}
</div>`).appendTo(row);
}
}
if (!$('.index .nav-link.active').is(':visible')) {
$('.index .nav-link:visible:not([href="#hash"])')[0].click();
}
$('#anitracker-anime-list-search').trigger('anitracker:search');
}
function updateAnimeEntries(entries) {
animeList.length = 0;
animeList.push(...entries);
}
function setSpinner(coverScreen) {
const elem = $(`
<div class="anitracker-filter-spinner ${coverScreen ? 'screen' : 'small'}">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
<span>0%</span>
</div>`);
if (coverScreen) elem.prependTo(document.body);
else elem.appendTo('.page-index h1');
}
function getSearchParams(filters, rules, inputParams = undefined) {
const params = inputParams || new URLSearchParams();
const values = [];
for (const type of ['genre','theme','type','demographic','status','season']) {
const foundFilters = filters.filter(f => f.type === type);
if (foundFilters.length === 0) {
params.delete(type);
continue;
}
values.push({filters: foundFilters, type: type});
}
for (const entry of values) {
if (entry.type === 'season') {
const value = entry.filters[0].value;
params.set('season', (entry.filters[0].exclude ? '!' : '') + `${getSeasonName(value.from.season)}-${value.from.year}..${getSeasonName(value.to.season)}-${value.to.year}`);
continue;
}
params.set(entry.type, entry.filters.map(g => (g.exclude ? '!' : '') + g.value).join(','));
}
const existingRules = getRulesListFromParams(params);
for (const rule of existingRules) {
params.delete(`rule-${rule.filterType}-${rule.ruleType}`);
}
const changedRules = getChangedRulesList(rules);
if (changedRules.length === 0) return params;
for (const rule of changedRules) {
params.set(`rule-${rule.filterType}-${rule.ruleType}`, rule.value);
}
return params;
}
function searchWithFilters(filters, screenSpinner) {
if ($('.anitracker-filter-spinner').length > 0) return; // If already searching
setSpinner(screenSpinner);
appliedFilters.length = 0;
appliedFilters.push(...JSON.parse(JSON.stringify(filters)));
setChangesToApply(false);
getFilteredList(filters).then(results => {
updateAnimeEntries(results);
layoutAnime(results);
$('.anitracker-filter-spinner').remove();
getSearchParams(filters, filterRules, searchParams); // Since a reference is passed, this will set the params
updateSearchParams();
});
}
const searchParamRuleRegex = /^rule\-(\w+)\-(include|exclude|combined)/;
function getRulesListFromParams(params) {
const rulesList = [];
for (const [key, value] of params.entries()) {
if (!searchParamRuleRegex.test(key) || !['any','or'].includes(value)) continue;
const parts = searchParamRuleRegex.exec(key);
if (filterRules[parts[1]] === undefined) continue;
rulesList.push({
filterType: parts[1],
ruleType: parts[2],
value: value
});
}
return rulesList;
}
function applyRulesList(rulesList) {
for (const rule of rulesList) {
filterRules[rule.filterType][rule.ruleType] = rule.value;
}
}
function getChangedRulesList(rules, type = undefined) {
const changed = [];
for (const [key, value] of Object.entries(rules)) {
if (type !== undefined && key !== type) continue;
if (value.include !== filterDefaultRules[key].include) {
changed.push({filterType: key, ruleType: 'include', value: value.include});
}
if (value.exclude !== filterDefaultRules[key].exclude) {
changed.push({filterType: key, ruleType: 'exclude', value: value.exclude});
}
if (![undefined,'and'].includes(value.combined)) {
changed.push({filterType: key, ruleType: 'combined', value: value.combined});
}
}
return changed;
}
function updateRuleButtons() {
const changedRules = getChangedRulesList(filterRules);
for (const type of Object.keys(filterRules)) {
const elems = getElemsFromFilterType(type);
const btn = elems.filterRuleButton;
if (btn === undefined || btn.length === 0) continue;
if (changedRules.find(r => r.filterType === type) === undefined) btn.removeClass('anitracker-active');
else btn.addClass('anitracker-active');
}
}
// Events
$('.anitracker-text-input').on('focus', (e) => {
const elem = $(e.currentTarget);
const filterType = elem.data('filter-type');
const elems = getElemsFromFilterType(filterType);
showDropdown(elems.dropdown, elems.parent);
clearPlaceholder(elems.input);
elem.css('width','').css('height','');
scrollToBottom(elems.scrollingDiv);
})
.on('blur', (e) => {
const elem = $(e.currentTarget);
const filterType = elem.data('filter-type');
const elems = getElemsFromFilterType(filterType);
checkCloseDropdown(elems);
if (elems.filterIcons.length === 0) {
addPlaceholder(elems.input, filterType);
elem.css('width','100%').css('height','100%');
}
})
.on('keydown', (e) => {
const elem = $(e.currentTarget);
const filterType = elem.data('filter-type');
const elems = getElemsFromFilterType(filterType);
if (e.key === 'Escape') {
elem.blur();
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
elems.dropdown.find('button:visible')[0]?.focus();
return;
}
const filterIcons = elems.filterIcons;
if (e.key === 'Backspace' && getInputText(elem) === '' && filterIcons.length > 0) {
removeFilter(getFilterDataFromElem($(filterIcons[filterIcons.length - 1])));
updateApplyButton();
}
setTimeout(() => {
const text = getInputText(elem).toLowerCase();
for (const btn of elems.dropdown.find('button')) {
const jqbtn = $(btn);
if (jqbtn.text().toLowerCase().includes(text)) {
jqbtn.show();
continue;
}
jqbtn.hide();
}
}, 1);
}).on('click', (e) => {
fixSelection($(e.currentTarget));
});
$('.anitracker-filter-dropdown:not(.special)>button').on('blur', (e) => {
const elem = $(e.currentTarget);
const filterType = elem.data('filter-type');
checkCloseDropdown(getElemsFromFilterType(filterType));
}).on('click', (e) => {
const elem = $(e.currentTarget);
const filter = getFilterDataFromElem(elem);
cycleFilter(filter);
const elems = getElemsFromFilterType(elem.data('filter-type'));
elems.input?.text('').keydown().blur();
scrollToBottom(elems.scrollingDiv);
});
$('.anitracker-filter-dropdown>button').on('keydown', (e) => {
const elem = $(e.currentTarget);
const filterType = elem.data('filter-type');
const elems = getElemsFromFilterType(filterType);
if (e.key === 'Escape') {
elem.blur();
return;
}
const direction = {
ArrowUp: -1,
ArrowDown: 1
}[e.key];
if (direction === undefined) return;
const activeButtons = elems.dropdown.find('button:visible');
let activeIndex = 0;
for (let i = 0; i < activeButtons.length; i++) {
const btn = activeButtons[i];
if (!$(btn).is(':focus')) continue;
activeIndex = i;
break;
}
const nextIndex = activeIndex + direction;
if (activeButtons[nextIndex] !== undefined) {
activeButtons[nextIndex].focus();
return;
}
if (direction === -1 && activeIndex === 0) {
elems.input?.focus();
return;
}
});
$('.anitracker-filter-input').on('click', (e) => {
const elem = $(e.target);
if (!elem.is('.anitracker-filter-input,.anitracker-applied-filters,.anitracker-filter-input>div')) return;
const filterType = $(e.currentTarget).data('filter-type');
const elems = getElemsFromFilterType(filterType);
setSelection(elems.input);
});
$('#anitracker-status-button').on('keydown', (e) => {
if (e.key !== 'ArrowDown') return;
const elems = getElemsFromFilterType('status');
elems.dropdown.find('button')[0]?.focus();
});
$('#anitracker-status-dropdown>button').on('click', (e) => {
const elem = $(e.currentTarget);
const filter = getFilterDataFromElem(elem);
addFilter(filter);
updateApplyButton();
});
$('#anitracker-apply-filters').on('click', () => {
searchWithFilters(selectedFilters, false);
});
$('.anitracker-filter-rules').on('click', (e) => {
const elem1 = $(e.currentTarget);
const filterType = elem1.data('filter-type');
const disableInclude = ['type','demographic'].includes(filterType) ? 'disabled' : '';
$('#anitracker-modal-body').empty();
$(`
<p>Rules for ${filterType} filters</p>
<div class="anitracker-filter-rule-selection" ${disableInclude} data-rule-type="include" style="background-color: #485057;">
<i class="fa fa-plus" aria-hidden="true"></i>
<span>Include:</span>
<div class="btn-group"><button ${disableInclude} title="Select this rule type">and</button><button ${disableInclude} title="Select this rule type">or</button></div>
</div>
<div class="anitracker-filter-rule-selection" data-rule-type="combined" style="display: flex;justify-content: center;">
<span>-</span>
<div class="btn-group"><button title="Select this rule type">and</button><button title="Select this rule type">or</button></div>
<span>-</span>
</div>
<div class="anitracker-filter-rule-selection" data-rule-type="exclude" style="background-color: #485057;">
<i class="fa fa-minus" aria-hidden="true"></i>
<span>Exclude:</span>
<div class="btn-group"><button title="Select this rule type">and</button><button title="Select this rule type">or</button></div>
</div>
<div style="display: flex;justify-content: center; margin-top: 10px;"><button class="btn btn-secondary anitracker-flat-button" id="anitracker-reset-filter-rules" title="Reset to defaults">Reset</button></div>
`).appendTo('#anitracker-modal-body');
function refreshBtnStates() {
const rules = filterRules[filterType];
for (const selec of $('.anitracker-filter-rule-selection')) {
const ruleType = $(selec).data('rule-type');
const rule = rules[ruleType];
const btns = $(selec).find('button').removeClass('anitracker-active');
if (rule === 'or') $(btns[1]).addClass('anitracker-active');
else $(btns[0]).addClass('anitracker-active');
}
}
$('.anitracker-filter-rule-selection button').on('click', (e) => {
const elem = $(e.currentTarget);
const ruleType = elem.parents().eq(1).data('rule-type');
const text = elem.text();
if (!['and','or'].includes(text)) return;
filterRules[filterType][ruleType] = text;
elem.parent().find('button').removeClass('anitracker-active');
elem.addClass('anitracker-active');
updateRuleButtons();
});
$('#anitracker-reset-filter-rules').on('click', () => {
filterRules[filterType] = JSON.parse(JSON.stringify(filterDefaultRules[filterType]));
refreshBtnStates();
updateRuleButtons();
});
refreshBtnStates();
openModal();
});
$('#anitracker-time-search-button').on('click', () => {
$('#anitracker-modal-body').empty();
$(`
<h5>Time interval</h5>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="anitracker-settings-enable-switch">
<label class="custom-control-label" for="anitracker-settings-enable-switch" title="Enable timeframe settings">Enable</label>
</div>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="anitracker-settings-invert-switch" disabled>
<label class="custom-control-label" for="anitracker-settings-invert-switch" title="Invert time range">Invert</label>
</div>
<br>
<div class="anitracker-season-group" id="anitracker-season-from">
<span>From:</span>
<div class="btn-group">
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-year-input" disabled placeholder="Year" type="number">
</div>
<div class="btn-group">
<button class="btn dropdown-toggle btn-secondary anitracker-season-dropdown-button" disabled data-bs-toggle="dropdown" data-toggle="dropdown" data-value="Spring">Spring</button>
<button class="btn btn-secondary" id="anitracker-season-copy-to-lower" title="Copy the 'from' season to the 'to' season">
<i class="fa fa-arrow-circle-down" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="anitracker-season-group" id="anitracker-season-to">
<span>To:</span>
<div class="btn-group">
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-year-input" disabled placeholder="Year" type="number">
</div>
<div class="btn-group">
<button class="btn dropdown-toggle btn-secondary anitracker-season-dropdown-button" disabled data-bs-toggle="dropdown" data-toggle="dropdown" data-value="Spring">Spring</button>
</div>
</div>
<br>
<div>
<div class="btn-group">
<button class="btn btn-primary" id="anitracker-modal-confirm-button">Save</button>
</div>
</div>`).appendTo('#anitracker-modal-body');
$('.anitracker-year-input').val(new Date().getFullYear());
$('#anitracker-settings-enable-switch').on('change', () => {
const enabled = $('#anitracker-settings-enable-switch').is(':checked');
$('.anitracker-season-group').find('input,button').prop('disabled', !enabled);
$('#anitracker-settings-invert-switch').prop('disabled', !enabled);
}).prop('checked', timeframeSettings.enabled).change();
$('#anitracker-settings-invert-switch').prop('checked', timeframeSettings.inverted);
$('#anitracker-season-copy-to-lower').on('click', () => {
const seasonName = $('#anitracker-season-from .anitracker-season-dropdown-button').data('value');
$('#anitracker-season-to .anitracker-year-input').val($('#anitracker-season-from .anitracker-year-input').val());
$('#anitracker-season-to .anitracker-season-dropdown-button').data('value', seasonName);
$('#anitracker-season-to .anitracker-season-dropdown-button').text(seasonName);
});
$(`<div class="dropdown-menu anitracker-dropdown-content anitracker-season-dropdown">`).insertAfter('.anitracker-season-dropdown-button');
['Winter','Spring','Summer','Fall'].forEach(g => { $(`<button ref="${g.toLowerCase()}">${g}</button>`).appendTo('.anitracker-season-dropdown') });
$('.anitracker-season-dropdown button').on('click', (e) => {
const pressed = $(e.target)
const btn = pressed.parents().eq(1).find('.anitracker-season-dropdown-button');
btn.data('value', pressed.text());
btn.text(pressed.text());
});
const currentSeason = getCurrentSeason();
if (timeframeSettings.from) {
$('#anitracker-season-from .anitracker-year-input').val(timeframeSettings.from.year.toString());
$('#anitracker-season-from .anitracker-season-dropdown button')[timeframeSettings.from.season].click();
}
else $('#anitracker-season-from .anitracker-season-dropdown button')[currentSeason].click();
if (timeframeSettings.to) {
$('#anitracker-season-to .anitracker-year-input').val(timeframeSettings.to.year.toString());
$('#anitracker-season-to .anitracker-season-dropdown button')[timeframeSettings.to.season].click();
}
else $('#anitracker-season-to .anitracker-season-dropdown button')[currentSeason].click();
$('#anitracker-modal-confirm-button').on('click', () => {
const enabled = $('#anitracker-settings-enable-switch').is(':checked');
const inverted = $('#anitracker-settings-invert-switch').is(':checked');
const from = {
year: +$('#anitracker-season-from .anitracker-year-input').val(),
season: getSeasonValue($('#anitracker-season-from').find('.anitracker-season-dropdown-button').data('value'))
}
const to = {
year: +$('#anitracker-season-to .anitracker-year-input').val(),
season: getSeasonValue($('#anitracker-season-to').find('.anitracker-season-dropdown-button').data('value'))
}
if (enabled) {
for (const input of $('.anitracker-year-input')) {
if (/^\d{4}$/.test($(input).val())) continue;
alert('[AnimePahe Improvements]\n\nYear values must both be 4 numbers.');
return;
}
if (to.year < from.year || (to.year === from.year && to.season < from.season)) {
alert('[AnimePahe Improvements]\n\nSeason times must be from oldest to newest.' + (to.season === 0 ? '\n(Winter is the first quarter of the year)' : ''));
return;
}
if (to.year - from.year > 100) {
alert('[AnimePahe Improvements]\n\nYear interval cannot be more than 100 years.');
return;
}
removeSeasonFilters(); // Put here so it doesn't remove existing filters if input is invalid
addFilter({
type: 'season',
value: {
from: from,
to: to
},
exclude: inverted
});
}
else {
removeSeasonFilters();
$('#anitracker-time-search-button').removeClass('anitracker-active');
}
updateApplyButton();
timeframeSettings.enabled = enabled;
timeframeSettings.inverted = inverted;
closeModal();
});
openModal();
});
$('#anitracker-random-anime').on('click', function(e) {
const elem = $(e.currentTarget);
elem.find('i').removeClass('fa-random').addClass('fa-refresh').css('animation', 'anitracker-spin 1s linear infinite');
getFilteredList(selectedFilters).then(results => {
elem.find('i').removeClass('fa-refresh').addClass('fa-random').css('animation', '');
const storage = getStorage();
storage.cache = filterSearchCache;
saveData(storage);
const params = getSearchParams(selectedFilters, filterRules);
params.set('anitracker-random', '1');
getRandomAnime(results, getSearchParamsString(params));
});
});
$.getScript('https://cdn.jsdelivr.net/npm/fuse.js@7.0.0', function() {
let typingTimer;
const elem = $('#anitracker-anime-list-search');
elem.prop('disabled', false).attr('placeholder', 'Search');
elem.on('anitracker:search', function() {
if ($(this).val() !== '') animeListSearch();
})
.on('keyup', function() {
clearTimeout(typingTimer);
typingTimer = setTimeout(animeListSearch, 150);
})
.on('keydown', function() {
clearTimeout(typingTimer);
});
function animeListSearch() {
const value = elem.val();
if (value === '') {
layoutAnime(JSON.parse(JSON.stringify(animeList)));
searchParams.delete('search');
}
else {
const matches = searchList(Fuse, animeList, value);
layoutTabless(matches);
searchParams.set('search', encodeURIComponent(value));
}
updateSearchParams();
}
const loadedParams = new URLSearchParams(window.location.search);
if (loadedParams.has('search')) {
elem.val(decodeURIComponent(loadedParams.get('search')));
animeListSearch();
}
}).fail(() => {
console.error("[AnimePahe Improvements] Fuse.js failed to load");
});
// From parameters
const paramRules = getRulesListFromParams(searchParams);
applyRulesList(paramRules);
updateRuleButtons();
const paramFilters = getFiltersFromParams(searchParams);
if (paramFilters.length === 0) return;
for (const filter of paramFilters) {
addFilter(filter);
}
searchWithFilters(selectedFilters, true);
}
// Search/index page
if (/^\/anime\/?$/.test(window.location.pathname)) {
loadIndexPage();
return;
}
function getAnimeList(page = $(document)) {
const animeList = [];
for (const anime of page.find('.col-12')) {
if (anime.children[0] === undefined || $(anime).hasClass('anitracker-filter-result') || $(anime).parent().attr('id') !== undefined) continue;
animeList.push({
name: $(anime.children[0]).text(),
link: anime.children[0].href,
html: $(anime).html()
});
}
return animeList;
}
function randint(min, max) { // min and max included
return Math.floor(Math.random() * (max - min + 1) + min);
}
function isEpisode(url = window.location.toString()) {
return url.includes('/play/');
}
function isAnime(url = window.location.pathname) {
return /^\/anime\/[\d\w\-]+$/.test(url);
}
function download(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function deleteEpisodesFromTracker(exclude, nameInput, id = undefined) {
const storage = getStorage();
const animeName = nameInput || getAnimeName();
const linkData = getStoredLinkData(storage);
storage.linkList = (() => {
if (id !== undefined) {
const found = storage.linkList.filter(g => g.type === 'episode' && g.animeId === id && g.episodeNum !== exclude);
if (found.length > 0) return storage.linkList.filter(g => !(g.type === 'episode' && g.animeId === id && g.episodeNum !== exclude));
}
return storage.linkList.filter(g => !(g.type === 'episode' && g.animeName === animeName && g.episodeNum !== exclude));
})();
storage.videoTimes = (() => {
if (id !== undefined) {
const found = storage.videoTimes.filter(g => g.animeId === id && g.episodeNum !== exclude);
if (found.length > 0) return storage.videoTimes.filter(g => !(g.animeId === id && g.episodeNum !== exclude));
}
return storage.videoTimes.filter(g => !(g.episodeNum !== exclude && stringSimilarity(g.animeName, animeName) > 0.81));
})();
if (exclude === undefined && id !== undefined) {
storage.videoSpeed = storage.videoSpeed.filter(g => g.animeId !== id);
}
saveData(storage);
}
function deleteEpisodeFromTracker(animeName, episodeNum, animeId = undefined) {
const storage = getStorage();
storage.linkList = (() => {
if (animeId !== undefined) {
const found = storage.linkList.find(g => g.type === 'episode' && g.animeId === animeId && g.episodeNum === episodeNum);
if (found !== undefined) return storage.linkList.filter(g => !(g.type === 'episode' && g.animeId === animeId && g.episodeNum === episodeNum));
}
return storage.linkList.filter(g => !(g.type === 'episode' && g.animeName === animeName && g.episodeNum === episodeNum));
})();
storage.videoTimes = (() => {
if (animeId !== undefined) {
const found = storage.videoTimes.find(g => g.animeId === animeId && g.episodeNum === episodeNum);
if (found !== undefined) return storage.videoTimes.filter(g => !(g.animeId === animeId && g.episodeNum === episodeNum));
}
return storage.videoTimes.filter(g => !(g.episodeNum === episodeNum && stringSimilarity(g.animeName, animeName) > 0.81));
})();
if (animeId !== undefined) {
const episodesRemain = storage.videoTimes.find(g => g.animeId === animeId) !== undefined;
if (!episodesRemain) {
storage.videoSpeed = storage.videoSpeed.filter(g => g.animeId !== animeId);
}
}
saveData(storage);
}
function getStoredLinkData(storage) {
if (isEpisode()) {
return storage.linkList.find(a => a.type == 'episode' && a.animeSession == animeSession && a.episodeSession == episodeSession);
}
return storage.linkList.find(a => a.type == 'anime' && a.animeSession == animeSession);
}
function getAnimeName() {
return isEpisode() ? /Watch (.*) - ([\d\.]+)(?:\-[\d\.]+)? Online/.exec($('.theatre-info h1').text())[1] : $($('.title-wrapper h1 span')[0]).text();
}
function getEpisodeNum() {
if (isEpisode()) return +(/Watch (.*) - ([\d\.]+)(?:\-[\d\.]+)? Online/.exec($('.theatre-info h1').text())[2]);
else return 0;
}
function sortAnimesChronologically(animeList) {
// Animes (plural)
animeList.sort((a, b) => {return getSeasonValue(a.season) > getSeasonValue(b.season) ? 1 : -1});
animeList.sort((a, b) => {return a.year > b.year ? 1 : -1});
return animeList;
}
function asyncGetResponseData(qurl) {
return new Promise((resolve, reject) => {
let req = new XMLHttpRequest();
req.open('GET', qurl, true);
req.onload = () => {
if (req.status === 200) {
resolve(JSON.parse(req.response).data);
return;
}
reject(undefined);
};
try {
req.send();
}
catch (err) {
console.error(err);
resolve(undefined);
}
});
}
function getResponseData(qurl) {
let req = new XMLHttpRequest();
req.open('GET', qurl, false);
try {
req.send();
}
catch (err) {
console.error(err);
return(undefined);
}
if (req.status === 200) {
return(JSON.parse(req.response).data);
}
return(undefined);
}
function getAnimeSessionFromUrl(url = window.location.toString()) {
return new RegExp('^(.*animepahe\.[a-z]+)?/(play|anime)/([^/?#]+)').exec(url)[3];
}
function getEpisodeSessionFromUrl(url = window.location.toString()) {
return new RegExp('^(.*animepahe\.[a-z]+)?/(play|anime)/([^/]+)/([^/?#]+)').exec(url)[4];
}
function makeSearchable(string) {
return encodeURIComponent(string.replace(' -',' '));
}
function getAnimeData(name = getAnimeName(), id = undefined, guess = false) {
const cached = (() => {
if (id !== undefined) return cachedAnimeData.find(a => a.id === id);
else return cachedAnimeData.find(a => a.title === name);
})();
if (cached !== undefined) {
return cached;
}
if (name.length === 0) return undefined;
const response = getResponseData('/api?m=search&q=' + makeSearchable(name));
if (response === undefined) return response;
for (const anime of response) {
if (id === undefined && anime.title === name) {
cachedAnimeData.push(anime);
return anime;
}
if (id !== undefined && anime.id === id) {
cachedAnimeData.push(anime);
return anime;
}
}
if (guess && response.length > 0) {
cachedAnimeData.push(response[0]);
return response[0];
}
return undefined;
}
async function asyncGetAnimeData(name = getAnimeName(), id) {
const cached = cachedAnimeData.find(a => a.id === id);
const response = cached === undefined ? await getResponseData('/api?m=search&q=' + makeSearchable(name)) : undefined;
return new Promise((resolve, reject) => {
if (cached !== undefined) {
resolve(cached);
return;
}
if (response === undefined) resolve(response);
for (const anime of response) {
if (anime.id === id) {
cachedAnimeData.push(anime);
resolve(anime);
}
}
reject(`Anime "${name}" not found`);
});
}
// For general animepahe pages that are not episode or anime pages
if (!url.includes('/play/') && !url.includes('/anime/') && !/anime[\/#]?[^\/]*([\?&][^=]+=[^\?^&])*$/.test(url)) {
$(`
<div id="anitracker">
</div>`).insertAfter('.notification-release');
addGeneralButtons();
updateSwitches();
return;
}
let animeSession = getAnimeSessionFromUrl();
let episodeSession = '';
if (isEpisode()) {
episodeSession = getEpisodeSessionFromUrl();
}
function getEpisodeSession(aSession, episodeNum) {
const request = new XMLHttpRequest();
request.open('GET', '/api?m=release&id=' + aSession, false);
request.send();
if (request.status !== 200) return undefined;
const response = JSON.parse(request.response);
return (() => {
for (let i = 1; i <= response.last_page; i++) {
const episodes = getResponseData(`/api?m=release&sort=episode_asc&page=${i}&id=${aSession}`);
if (episodes === undefined) return undefined;
const episode = episodes.find(a => a.episode === episodeNum);
if (episode === undefined) continue;
return episode.session;
}
})();
}
function refreshSession(from404 = false) {
/* Return codes:
* 0: ok!
* 1: couldn't find stored session at 404 page
* 2: couldn't get anime data
* 3: couldn't get episode session
* 4: idk
*/
const storage = getStorage();
const bobj = getStoredLinkData(storage);
let name = '';
let episodeNum = 0;
if (bobj === undefined && from404) return 1;
if (bobj !== undefined) {
name = bobj.animeName;
episodeNum = bobj.episodeNum;
}
else {
name = getAnimeName();
episodeNum = getEpisodeNum();
}
if (isEpisode()) {
const animeData = getAnimeData(name, bobj?.animeId, true);
if (animeData === undefined) return 2;
if (bobj?.animeId === undefined && animeData.title !== name && !refreshGuessWarning(name, animeData.title)) {
return 2;
}
const episodeSession = getEpisodeSession(animeData.session, episodeNum);
if (episodeSession === undefined) return 3;
if (bobj !== undefined) {
storage.linkList = storage.linkList.filter(g => !(g.type === 'episode' && g.animeSession === bobj.animeSession && g.episodeSession === bobj.episodeSession));
}
saveData(storage);
window.location.replace('/play/' + animeData.session + '/' + episodeSession + window.location.search);
return 0;
}
else if (bobj !== undefined && bobj.animeId !== undefined) {
storage.linkList = storage.linkList.filter(g => !(g.type === 'anime' && g.animeSession === bobj.animeSession));
saveData(storage);
window.location.replace('/a/' + bobj.animeId);
return 0;
}
else {
if (bobj !== undefined) {
storage.linkList = storage.linkList.filter(g => !(g.type === 'anime' && g.animeSession === bobj.animeSession));
saveData(storage);
}
let animeData = getAnimeData(name, undefined, true);
if (animeData === undefined) return 2;
if (animeData.title !== name && !refreshGuessWarning(name, animeData.title)) {
return 2;
}
window.location.replace('/a/' + animeData.id);
return 0;
}
return 4;
}
function refreshGuessWarning(name, title) {
return confirm(`[AnimePahe Improvements]\n\nAn exact match with the anime name "${name}" couldn't be found. Go to "${title}" instead?`);
}
const obj = getStoredLinkData(initialStorage);
if (isEpisode() && !is404) {
theatreMode(initialStorage.settings.theatreMode);
$('#downloadMenu').changeElementType('button');
}
console.log('[AnimePahe Improvements]', obj, animeSession, episodeSession);
function setSessionData() {
const animeName = getAnimeName();
const storage = getStorage();
if (isEpisode()) {
storage.linkList.push({
animeId: getAnimeData(animeName)?.id,
animeSession: animeSession,
episodeSession: episodeSession,
type: 'episode',
animeName: animeName,
episodeNum: getEpisodeNum()
});
}
else {
storage.linkList.push({
animeId: getAnimeData(animeName)?.id,
animeSession: animeSession,
type: 'anime',
animeName: animeName
});
}
if (storage.linkList.length > 1000) {
storage.linkList.splice(0,1);
}
saveData(storage);
}
if (obj === undefined && !is404) {
if (!isRandomAnime()) setSessionData();
}
else if (obj !== undefined && is404) {
document.title = "Refreshing session... :: animepahe";
$('.text-center h1').text('Refreshing session, please wait...');
const code = refreshSession(true);
if (code === 1) {
$('.text-center h1').text('Couldn\'t refresh session: Link not found in tracker');
}
else if (code === 2) {
$('.text-center h1').text('Couldn\'t refresh session: Couldn\'t get anime data');
}
else if (code === 3) {
$('.text-center h1').text('Couldn\'t refresh session: Couldn\'t get episode data');
}
else if (code !== 0) {
$('.text-center h1').text('Couldn\'t refresh session: An unknown error occured');
}
if ([2,3].includes(code)) {
if (obj.episodeNum !== undefined) {
$(`<h3>
Try finding the episode using the following info:
<br>Anime name: ${obj.animeName}
<br>Episode: ${obj.episodeNum}
</h3>`).insertAfter('.text-center h1');
}
else {
$(`<h3>
Try finding the anime using the following info:
<br>Anime name: ${obj.animeName}
</h3>`).insertAfter('.text-center h1');
}
}
return;
}
else if (obj === undefined && is404) {
if (document.referrer.length > 0) {
const bobj = (() => {
if (!/\/play\/.+/.test(document.referrer) && !/\/anime\/.+/.test(document.referrer)) {
return true;
}
const session = getAnimeSessionFromUrl(document.referrer);
if (isEpisode(document.referrer)) {
return initialStorage.linkList.find(a => a.type === 'episode' && a.animeSession === session && a.episodeSession === getEpisodeSessionFromUrl(document.referrer));
}
else {
return initialStorage.linkList.find(a => a.type === 'anime' && a.animeSession === session);
}
})();
if (bobj !== undefined) {
const prevUrl = new URL(document.referrer);
const params = new URLSearchParams(prevUrl);
params.set('ref','404');
prevUrl.search = params.toString();
windowOpen(prevUrl.toString(), '_self');
return;
}
}
$('.text-center h1').text('Cannot refresh session: Link not stored in tracker');
return;
}
function getSubInfo(str) {
const match = /^\b([^·]+)·\s*(\d{2,4})p(.*)$/.exec(str);
return {
name: match[1],
quality: +match[2],
other: match[3]
};
}
// Set the quality to best automatically
function bestVideoQuality() {
if (!isEpisode()) return;
const currentSub = getStoredLinkData(getStorage()).subInfo || getSubInfo($('#resolutionMenu .active').text());
let index = -1;
for (let i = 0; i < $('#resolutionMenu').children().length; i++) {
const sub = $('#resolutionMenu').children()[i];
const subInfo = getSubInfo($(sub).text());
if (subInfo.name !== currentSub.name || subInfo.other !== currentSub.other) continue;
if (subInfo.quality >= currentSub.quality) index = i;
}
if (index === -1) {
return;
}
const newSub = $('#resolutionMenu').children()[index];
if (!["","Loading..."].includes($('#fansubMenu').text())) {
if ($(newSub).text() === $('#resolutionMenu .active').text()) return;
newSub.click();
return;
}
new MutationObserver(function(mutationList, observer) {
newSub.click();
observer.disconnect();
}).observe($('#fansubMenu')[0], { childList: true });
}
function setIframeUrl(url) {
$('.embed-responsive-item').remove();
$(`
<iframe class="embed-responsive-item" scrolling="no" allowfullscreen="" allowtransparency="" src="${url}"></iframe>
`).prependTo('.embed-responsive');
$('.embed-responsive-item')[0].contentWindow.focus();
}
// Fix the quality dropdown buttons
if (isEpisode()) {
new MutationObserver(function(mutationList, observer) {
$('.click-to-load').remove();
$('#resolutionMenu').off('click');
$('#resolutionMenu').on('click', (el) => {
const targ = $(el.target);
if (targ.data('src') === undefined) return;
setIframeUrl(targ.data('src'));
$('#resolutionMenu .active').removeClass('active');
targ.addClass('active');
$('#fansubMenu').html(targ.html());
const storage = getStorage();
const data = getStoredLinkData(storage);
data.subInfo = getSubInfo(targ.text());
saveData(storage);
$.cookie('res', targ.data('resolution'), {
expires: 365,
path: '/'
});
$.cookie('aud', targ.data('audio'), {
expires: 365,
path: '/'
});
$.cookie('av1', targ.data('av1'), {
expires: 365,
path: '/'
});
});
observer.disconnect();
}).observe($('#fansubMenu')[0], { childList: true });
if (initialStorage.settings.bestQuality === true) {
bestVideoQuality();
}
else if (!["","Loading..."].includes($('#fansubMenu').text())) {
$('#resolutionMenu .active').click();
} else {
new MutationObserver(function(mutationList, observer) {
$('#resolutionMenu .active').click();
observer.disconnect();
}).observe($('#fansubMenu')[0], { childList: true });
}
const timeArg = paramArray.find(a => a[0] === 'time');
if (timeArg !== undefined) {
applyTimeArg(timeArg);
}
}
function applyTimeArg(timeArg) {
const time = timeArg[1];
function check() {
if ($('.embed-responsive-item').attr('src') !== undefined) done();
else setTimeout(check, 100);
}
setTimeout(check, 100);
function done() {
setIframeUrl(stripUrl($('.embed-responsive-item').attr('src')) + '?time=' + time);
window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
}
}
function getTrackerDiv() {
return $(`
<div id="anitracker">
<button class="btn btn-dark" id="anitracker-refresh-session" title="Refresh the session for the current page">
<i class="fa fa-refresh" aria-hidden="true"></i>
&nbsp;Refresh Session
</button>
</div>`);
}
async function asyncGetAllEpisodes(session, sort = "asc") {
const episodeList = [];
const request = new XMLHttpRequest();
request.open('GET', `/api?m=release&sort=episode_${sort}&id=` + session, true);
return new Promise((resolve, reject) => {
request.onload = () => {
if (request.status !== 200) {
reject("Received response code " + request.status);
return;
}
const response = JSON.parse(request.response);
if (response.current_page === response.last_page) {
episodeList.push(...response.data);
}
else for (let i = 1; i <= response.last_page; i++) {
asyncGetResponseData(`/api?m=release&sort=episode_${sort}&page=${i}&id=${session}`).then((episodes) => {
if (episodes === undefined || episodes.length === 0) return;
episodeList.push(...episodes);
});
}
resolve(episodeList);
};
request.send();
});
}
async function getRelationData(session, relationType) {
const request = new XMLHttpRequest();
request.open('GET', '/anime/' + session, false);
request.send();
const page = request.status === 200 ? $(request.response) : {};
if (Object.keys(page).length === 0) return undefined;
const relationDiv = (() => {
for (const div of page.find('.anime-relation .col-12')) {
if ($(div).find('h4 span').text() !== relationType) continue;
return $(div);
break;
}
return undefined;
})();
if (relationDiv === undefined) return undefined;
const relationSession = new RegExp('^.*animepahe\.[a-z]+/anime/([^/]+)').exec(relationDiv.find('a')[0].href)[1];
return new Promise(resolve => {
const episodeList = [];
asyncGetAllEpisodes(relationSession).then((episodes) => {
episodeList.push(...episodes);
if (episodeList.length === 0) {
resolve(undefined);
return;
}
resolve({
episodes: episodeList,
name: $(relationDiv.find('h5')[0]).text(),
poster: relationDiv.find('img').attr('data-src').replace('.th',''),
session: relationSession
});
});
});
}
function hideSpinner(t, parents = 1) {
$(t).parents(`:eq(${parents})`).find('.anitracker-download-spinner').hide();
}
if (isEpisode()) {
getTrackerDiv().appendTo('.anime-note');
$('.prequel,.sequel').addClass('anitracker-thumbnail');
$(`
<span relationType="Prequel" class="dropdown-item anitracker-relation-link" id="anitracker-prequel-link">
Previous Anime
</span>`).prependTo('.episode-menu #scrollArea');
$(`
<span relationType="Sequel" class="dropdown-item anitracker-relation-link" id="anitracker-sequel-link">
Next Anime
</span>`).appendTo('.episode-menu #scrollArea');
$('.anitracker-relation-link').on('click', function() {
if (this.href !== undefined) {
$(this).off();
return;
}
$(this).parents(':eq(2)').find('.anitracker-download-spinner').show();
const animeData = getAnimeData();
if (animeData === undefined) {
hideSpinner(this, 2);
return;
}
const relationType = $(this).attr('relationType');
getRelationData(animeData.session, relationType).then((relationData) => {
if (relationData === undefined) {
hideSpinner(this, 2);
alert(`[AnimePahe Improvements]\n\nNo ${relationType.toLowerCase()} found for this anime.`);
$(this).remove();
return;
}
const episodeSession = relationType === 'Prequel' ? relationData.episodes[relationData.episodes.length-1].session : relationData.episodes[0].session;
windowOpen(`/play/${relationData.session}/${episodeSession}`, '_self');
hideSpinner(this, 2);
});
});
if ($('.prequel').length === 0) setPrequelPoster();
if ($('.sequel').length === 0) setSequelPoster();
} else {
getTrackerDiv().insertAfter('.anime-content');
}
async function setPrequelPoster() {
const relationData = await getRelationData(animeSession, 'Prequel');
if (relationData === undefined) {
$('#anitracker-prequel-link').remove();
return;
}
const relationLink = `/play/${relationData.session}/${relationData.episodes[relationData.episodes.length-1].session}`;
$(`
<div class="prequel hidden-sm-down anitracker-thumbnail">
<a href="${relationLink}" title="${toHtmlCodes("Play Last Episode of " + relationData.name)}">
<img style="filter: none;" src="${relationData.poster}" data-src="${relationData.poster}" alt="">
</a>
<i class="fa fa-chevron-left" aria-hidden="true"></i>
</div>`).appendTo('.player');
$('#anitracker-prequel-link').attr('href', relationLink);
$('#anitracker-prequel-link').text(relationData.name);
$('#anitracker-prequel-link').changeElementType('a');
// If auto-clear is on, delete this prequel episode from the tracker
if (getStorage().settings.autoDelete === true) {
deleteEpisodesFromTracker(undefined, relationData.name);
}
}
async function setSequelPoster() {
const relationData = await getRelationData(animeSession, 'Sequel');
if (relationData === undefined) {
$('#anitracker-sequel-link').remove();
return;
}
const relationLink = `/play/${relationData.session}/${relationData.episodes[0].session}`;
$(`
<div class="sequel hidden-sm-down anitracker-thumbnail">
<a href="${relationLink}" title="${toHtmlCodes("Play First Episode of " + relationData.name)}">
<img style="filter: none;" src="${relationData.poster}" data-src="${relationData.poster}" alt="">
</a>
<i class="fa fa-chevron-right" aria-hidden="true"></i>
</div>`).appendTo('.player');
$('#anitracker-sequel-link').attr('href', relationLink);
$('#anitracker-sequel-link').text(relationData.name);
$('#anitracker-sequel-link').changeElementType('a');
}
if (!isEpisode() && $('#anitracker') != undefined) {
$('#anitracker').attr('style', "max-width: 1100px;margin-left: auto;margin-right: auto;margin-bottom: 20px;");
}
$('#anitracker-refresh-session').on('click', function(e) {
const elem = $('#anitracker-refresh-session');
let timeout = temporaryHtmlChange(elem, 2200, 'Waiting...');
const result = refreshSession();
if (result === 0) {
temporaryHtmlChange(elem, 2200, '<i class="fa fa-refresh" aria-hidden="true" style="animation: anitracker-spin 1s linear infinite;"></i>&nbsp;&nbsp;Refreshing...', timeout);
}
else if ([2,3].includes(result)) {
temporaryHtmlChange(elem, 2200, 'Failed: Couldn\'t find session', timeout);
}
else {
temporaryHtmlChange(elem, 2200, 'Failed.', timeout);
}
});
if (isEpisode()) {
// Replace the download buttons with better ones
if ($('#pickDownload a').length > 0) replaceDownloadButtons();
else {
new MutationObserver(function(mutationList, observer) {
replaceDownloadButtons();
observer.disconnect();
}).observe($('#pickDownload')[0], { childList: true });
}
$(document).on('blur', () => {
$('.dropdown-menu.show').removeClass('show');
});
(() => {
const storage = getStorage();
const foundNotifEpisode = storage.notifications.episodes.find(a => a.session === episodeSession);
if (foundNotifEpisode !== undefined) {
foundNotifEpisode.watched = true;
saveData(storage);
}
})();
}
function replaceDownloadButtons() {
for (const aTag of $('#pickDownload a')) {
$(aTag).changeElementType('span');
}
$('#pickDownload span').on('click', function(e) {
let request = new XMLHttpRequest();
//request.open('GET', `https://opsalar.000webhostapp.com/animepahe.php?url=${$(this).attr('href')}`, true);
request.open('GET', $(this).attr('href'), true);
try {
request.send();
$(this).parents(':eq(1)').find('.anitracker-download-spinner').show();
}
catch (err) {
windowOpen($(this).attr('href'));
}
const dlBtn = $(this);
request.onload = function(e) {
hideSpinner(dlBtn);
if (request.readyState !== 4 || request.status !== 200 ) {
windowOpen(dlBtn.attr('href'));
return;
}
const htmlText = request.response;
const link = /https:\/\/kwik.\w+\/f\/[^"]+/.exec(htmlText);
if (link) {
dlBtn.attr('href', link[0]);
dlBtn.off();
dlBtn.changeElementType('a');
windowOpen(link[0]);
}
else windowOpen(dlBtn.attr('href'));
};
});
}
function stripUrl(url) {
if (url === undefined) {
console.error('[AnimePahe Improvements] stripUrl was used with undefined URL');
return url;
}
const loc = new URL(url);
return loc.origin + loc.pathname;
}
function temporaryHtmlChange(elem, delay, html, timeout = undefined) {
if (timeout !== undefined) clearTimeout(timeout);
if ($(elem).attr('og-html') === undefined) {
$(elem).attr('og-html', $(elem).html());
}
elem.html(html);
return setTimeout(() => {
$(elem).html($(elem).attr('og-html'));
}, delay);
}
$(`
<button class="btn btn-dark" id="anitracker-clear-from-tracker" title="Remove this page from the session tracker">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Clear from Tracker
</button>`).appendTo('#anitracker');
$('#anitracker-clear-from-tracker').on('click', function() {
const animeName = getAnimeName();
if (isEpisode()) {
deleteEpisodeFromTracker(animeName, getEpisodeNum(), getAnimeData().id);
if ($('.embed-responsive-item').length > 0) {
const storage = getStorage();
const videoUrl = stripUrl($('.embed-responsive-item').attr('src'));
for (const videoData of storage.videoTimes) {
if (!videoData.videoUrls.includes(videoUrl)) continue;
const index = storage.videoTimes.indexOf(videoData);
storage.videoTimes.splice(index, 1);
saveData(storage);
break;
}
}
}
else {
const storage = getStorage();
storage.linkList = storage.linkList.filter(a => !(a.type === 'anime' && a.animeName === animeName));
saveData(storage);
}
temporaryHtmlChange($('#anitracker-clear-from-tracker'), 1500, 'Cleared!');
});
function setCoverBlur(img) {
const cover = $('.anime-cover');
const ratio = cover.width()/img.width;
if (ratio <= 1) return;
cover.css('filter', `blur(${(ratio*Math.max((img.height/img.width)**2, 1))*1.6}px)`);
}
function improvePoster() {
if ($('.anime-poster .youtube-preview').length === 0) {
$('.anime-poster .poster-image').attr('target','_blank');
return;
}
$('.anime-poster .youtube-preview').removeAttr('href');
$(`
<a style="display:block;" target="_blank" href="${$('.anime-poster img').attr('src')}">
View full poster
</a>`).appendTo('.anime-poster');
}
if (isAnime()) {
if ($('.anime-poster img').attr('src') !== undefined) {
improvePoster();
}
else $('.anime-poster img').on('load', (e) => {
improvePoster();
$(e.target).off('load');
});
$(`
<button class="btn btn-dark" id="anitracker-clear-episodes-from-tracker" title="Clear all episodes from this anime from the session tracker">
<i class="fa fa-trash" aria-hidden="true"></i>
<i class="fa fa-window-maximize" aria-hidden="true"></i>
&nbsp;Clear Episodes from Tracker
</button>`).appendTo('#anitracker');
$('#anitracker-clear-episodes-from-tracker').on('click', function() {
const animeData = getAnimeData();
deleteEpisodesFromTracker(undefined, animeData.title, animeData.id);
temporaryHtmlChange($('#anitracker-clear-episodes-from-tracker'), 1500, 'Cleared!');
});
const storedObj = getStoredLinkData(initialStorage);
if (storedObj === undefined || storedObj?.coverImg === undefined) updateAnimeCover();
else
{
new MutationObserver(function(mutationList, observer) {
$('.anime-cover').css('background-image', `url("${storedObj.coverImg}")`);
$('.anime-cover').addClass('anitracker-replaced-cover');
const img = new Image();
img.src = storedObj.coverImg;
img.onload = () => {
setCoverBlur(img);
};
observer.disconnect();
}).observe($('.anime-cover')[0], { attributes: true });
}
if (isRandomAnime()) {
const sourceParams = new URLSearchParams(window.location.search);
window.history.replaceState({}, document.title, "/anime/" + animeSession);
const storage = getStorage();
if (storage.cache) {
for (const [key, value] of Object.entries(storage.cache)) {
filterSearchCache[key] = value;
}
delete storage.cache;
saveData(storage);
}
$(`
<div style="margin-left: 240px;">
<div class="btn-group">
<button class="btn btn-dark" id="anitracker-reroll-button"><i class="fa fa-random" aria-hidden="true"></i>&nbsp;Reroll Anime</button>
</div>
<div class="btn-group">
<button class="btn btn-dark" id="anitracker-save-session"><i class="fa fa-floppy-o" aria-hidden="true"></i>&nbsp;Save Session</button>
</div>
</div>`).appendTo('.title-wrapper');
$('#anitracker-reroll-button').on('click', function() {
$(this).text('Rerolling...');
const sourceFilters = new URLSearchParams(sourceParams.toString());
getFilteredList(getFiltersFromParams(sourceFilters)).then((animeList) => {
const storage = getStorage();
storage.cache = filterSearchCache;
saveData(storage);
getRandomAnime(animeList, '?' + sourceParams.toString(), '_self');
});
});
$('#anitracker-save-session').on('click', function() {
setSessionData();
$('#anitracker-save-session').off();
$(this).text('Saved!');
setTimeout(() => {
$(this).parent().remove();
}, 1500);
});
}
new MutationObserver(function(mutationList, observer) {
const pageNum = (() => {
const elem = $('.pagination');
if (elem.length == 0) return 1;
return +/^(\d+)/.exec($('.pagination').find('.page-item.active span').text())[0];
})();
const episodeSort = $('.episode-bar .btn-group-toggle .active').text().trim();
const episodes = getResponseData(`/api?m=release&sort=episode_${episodeSort}&page=${pageNum}&id=${animeSession}`);
if (episodes === undefined) return undefined;
const episodeElements = $('.episode-wrap');
for (let i = 0; i < episodeElements.length; i++) {
const elem = $(episodeElements[i]);
const date = new Date(episodes[i].created_at + " UTC");
$(`
<a class="anitracker-episode-time" href="${$(elem.find('a.play')).attr('href')}" tabindex="-1" title="${date.toDateString() + " " + date.toLocaleTimeString()}">${date.toLocaleDateString()}</a>
`).appendTo(elem.find('.episode-title-wrap'));
}
observer.disconnect();
setTimeout(observer.observe($('.episode-list-wrapper')[0], { childList: true, subtree: true }), 1);
}).observe($('.episode-list-wrapper')[0], { childList: true, subtree: true });
// Bookmark icon
const animename = getAnimeName();
const animeid = getAnimeData(animename).id;
$('h1 .fa').remove();
const notifIcon = (() => {
if (initialStorage.notifications.anime.find(a => a.name === animename) !== undefined) return true;
for (const info of $('.anime-info p>strong')) {
if (!$(info).text().startsWith('Status:')) continue;
return $(info).text().includes("Not yet aired") || $(info).find('a').text() === "Currently Airing";
}
return false;
})() ?
`<i title="Add to episode feed" class="fa fa-bell anitracker-title-icon anitracker-notifications-toggle">
<i style="display: none;" class="fa fa-check anitracker-title-icon-check" aria-hidden="true"></i>
</i>` : '';
$(`
<i title="Bookmark this anime" class="fa fa-bookmark anitracker-title-icon anitracker-bookmark-toggle">
<i style="display: none;" class="fa fa-check anitracker-title-icon-check" aria-hidden="true"></i>
</i>${notifIcon}<a href="/a/${animeid}" title="Get Link" class="fa fa-link btn anitracker-title-icon" data-toggle="modal" data-target="#modalBookmark"></a>
`).appendTo('.title-wrapper>h1');
if (initialStorage.bookmarks.find(g => g.id === animeid) !== undefined) {
$('.anitracker-bookmark-toggle .anitracker-title-icon-check').show();
}
if (initialStorage.notifications.anime.find(g => g.id === animeid) !== undefined) {
$('.anitracker-notifications-toggle .anitracker-title-icon-check').show();
}
$('.anitracker-bookmark-toggle').on('click', (e) => {
const check = $(e.currentTarget).find('.anitracker-title-icon-check');
if (toggleBookmark(animeid, animename)) {
check.show();
return;
}
check.hide();
});
$('.anitracker-notifications-toggle').on('click', (e) => {
const check = $(e.currentTarget).find('.anitracker-title-icon-check');
if (toggleNotifications(animename, animeid)) {
check.show();
return;
}
check.hide();
});
}
function getRandomAnime(list, args, openType = '_blank') {
if (list.length === 0) {
alert("[AnimePahe Improvements]\n\nThere is no anime that matches the selected filters.");
return;
}
const random = randint(0, list.length-1);
windowOpen(list[random].link + args, openType);
}
function isRandomAnime() {
return new URLSearchParams(window.location.search).has('anitracker-random');
}
function getBadCovers() {
const storage = getStorage();
return ['https://s.pximg.net/www/images/pixiv_logo.png',
'https://st.deviantart.net/minish/main/logo/card_black_large.png',
'https://www.wcostream.com/wp-content/themes/animewp78712/images/logo.gif',
'https://s.pinimg.com/images/default_open_graph',
'https://share.redd.it/preview/post/',
'https://i.redd.it/o0h58lzmax6a1.png',
'https://ir.ebaystatic.com/cr/v/c1/ebay-logo',
'https://i.ebayimg.com/images/g/7WgAAOSwQ7haxTU1/s-l1600.jpg',
'https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/RT_TwitterCard',
'https://m.media-amazon.com/images/G/01/social_share/amazon_logo',
'https://zoro.to/images/capture.png',
'https://cdn.myanimelist.net/img/sp/icon/twitter-card.png',
'https://s2.bunnycdn.ru/assets/sites/animesuge/images/preview.jpg',
'https://s2.bunnycdn.ru/assets/sites/anix/preview.jpg',
'https://cdn.myanimelist.net/images/company_no_picture.png',
'https://myanimeshelf.com/eva2/handlers/generateSocialImage.php',
'https://cdn.myanimelist.net/img/sp/icon/apple-touch-icon',
'https://m.media-amazon.com/images/G/01/imdb/images/social',
'https://forums.animeuknews.net/styles/default/',
'https://honeysanime.com/wp-content/uploads/2016/12/facebook_cover_2016_851x315.jpg',
'https://fi.somethingawful.com/images/logo.png',
'https://static.hidive.com/misc/HIDIVE-Logo-White.png',
...storage.badCovers];
}
async function updateAnimeCover() {
$(`<div id="anitracker-cover-spinner">
<div class="spinner-border text-danger" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>`).prependTo('.anime-cover');
const request = new XMLHttpRequest();
let beforeYear = 2022;
for (const info of $('.anime-info p')) {
if (!$(info).find('strong').html().startsWith('Season:')) continue;
const year = +/(\d+)$/.exec($(info).find('a').text())[0];
if (year >= beforeYear) beforeYear = year + 1;
}
request.open('GET', 'https://customsearch.googleapis.com/customsearch/v1?key=AIzaSyCzrHsVOqJ4vbjNLpGl8XZcxB49TGDGEFk&cx=913e33346cc3d42bf&tbs=isz:l&q=' + encodeURIComponent(getAnimeName()) + '%20anime%20hd%20wallpaper%20-phone%20-ai%20before:' + beforeYear, true);
request.onload = function() {
if (request.status !== 200) {
$('#anitracker-cover-spinner').remove();
return;
}
if ($('.anime-cover').css('background-image').length > 10) {
decideAnimeCover(request.response);
}
else {
new MutationObserver(function(mutationList, observer) {
if ($('.anime-cover').css('background-image').length <= 10) return;
decideAnimeCover(request.response);
observer.disconnect();
}).observe($('.anime-cover')[0], { attributes: true });
}
};
request.send();
}
function trimHttp(string) {
return string.replace(/^https?:\/\//,'');
}
async function setAnimeCover(src) {
return new Promise(resolve => {
$('.anime-cover').css('background-image', `url("${storedObj.coverImg}")`);
$('.anime-cover').addClass('anitracker-replaced-cover');
const img = new Image();
img.src = src;
img.onload = () => {
setCoverBlur(img);
}
$('.anime-cover').addClass('anitracker-replaced-cover');
$('.anime-cover').css('background-image', `url("${src}")`);
$('.anime-cover').attr('image', src);
$('#anitracker-replace-cover').remove();
$(`<button class="btn btn-dark" id="anitracker-replace-cover" title="Use another cover instead">
<i class="fa fa-refresh" aria-hidden="true"></i>
</button>`).appendTo('.anime-cover');
$('#anitracker-replace-cover').on('click', e => {
const storage = getStorage();
storage.badCovers.push($('.anime-cover').attr('image'));
saveData(storage);
updateAnimeCover();
$(e.target).off();
playAnimation($(e.target).find('i'), 'spin', 'infinite', 1);
});
setCoverBlur(image);
});
}
async function decideAnimeCover(response) {
const badCovers = getBadCovers();
const candidates = [];
let results = [];
try {
results = JSON.parse(response).items;
}
catch (e) {
return;
}
if (results === undefined) {
$('#anitracker-cover-spinner').remove();
return;
}
for (const result of results) {
let imgUrl = result['pagemap']?.['metatags']?.[0]?.['og:image'] ||
result['pagemap']?.['cse_image']?.[0]?.['src'] || result['pagemap']?.['webpage']?.[0]?.['image'] ||
result['pagemap']?.['metatags']?.[0]?.['twitter:image:src'];
const width = result['pagemap']?.['cse_thumbnail']?.[0]?.['width'];
const height = result['pagemap']?.['cse_thumbnail']?.[0]?.['height'];
if (imgUrl === undefined || height < 100 || badCovers.find(a=> trimHttp(imgUrl).startsWith(trimHttp(a))) !== undefined || imgUrl.endsWith('.gif')) continue;
if (imgUrl.startsWith('https://static.wikia.nocookie.net')) {
imgUrl = imgUrl.replace(/\/revision\/latest.*\?cb=\d+$/, '');
}
candidates.push({
src: imgUrl,
width: width,
height: height,
aspectRatio: width / height
});
}
if (candidates.length === 0) return;
candidates.sort((a, b) => {return a.aspectRatio < b.aspectRatio ? 1 : -1});
if (candidates[0].src.includes('"')) return;
const originalBg = $('.anime-cover').css('background-image');
function badImg() {
$('.anime-cover').css('background-image', originalBg);
const storage = getStorage();
for (const anime of storage.linkList) {
if (anime.type === 'anime' && anime.animeSession === animeSession) {
anime.coverImg = /^url\("?([^"]+)"?\)$/.exec(originalBg)[1];
break;
}
}
saveData(storage);
$('#anitracker-cover-spinner').remove();
}
const image = new Image();
image.onload = () => {
if (image.width >= 250) {
$('.anime-cover').addClass('anitracker-replaced-cover');
$('.anime-cover').css('background-image', `url("${candidates[0].src}")`);
$('.anime-cover').attr('image', candidates[0].src);
setCoverBlur(image);
const storage = getStorage();
for (const anime of storage.linkList) {
if (anime.type === 'anime' && anime.animeSession === animeSession) {
anime.coverImg = candidates[0].src;
break;
}
}
saveData(storage);
$('#anitracker-cover-spinner').remove();
}
else badImg();
};
image.addEventListener('error', function() {
badImg();
});
image.src = candidates[0].src;
}
function hideThumbnails() {
$('.main').addClass('anitracker-hide-thumbnails');
}
function resetPlayer() {
setIframeUrl(stripUrl($('.embed-responsive-item').attr('src')));
}
function addGeneralButtons() {
$(`
<button class="btn btn-dark" id="anitracker-show-data" title="View and handle stored sessions and video progress">
<i class="fa fa-floppy-o" aria-hidden="true"></i>
&nbsp;Manage Data...
</button>
<button class="btn btn-dark" id="anitracker-settings" title="Settings">
<i class="fa fa-sliders" aria-hidden="true"></i>
&nbsp;Settings...
</button>`).appendTo('#anitracker');
$('#anitracker-settings').on('click', () => {
$('#anitracker-modal-body').empty();
$('<span style="display:block;">Video player:</span>').appendTo('#anitracker-modal-body');
addOptionSwitch('autoPlayVideo', 'Auto-Play Video', 'Automatically play the video when it is loaded.');
addOptionSwitch('theatreMode', 'Theatre Mode', 'Expand the video player for a better experience on bigger screens.');
addOptionSwitch('bestQuality', 'Default to Best Quality', 'Automatically select the best resolution quality available.');
addOptionSwitch('seekThumbnails', 'Seek Thumbnails', 'Show thumbnail images while seeking through the progress bar. May cause performance issues on weak systems.');
addOptionSwitch('seekPoints', 'Seek Points', 'Show points on the progress bar.');
addOptionSwitch('skipButton', 'Skip Button', 'Show a button to skip sections of episodes.');
if (isEpisode()) {
$(`
<div class="btn-group">
<button class="btn btn-secondary" id="anitracker-reset-player" title="Reset the video player">
<i class="fa fa-rotate-right" aria-hidden="true"></i>
&nbsp;Reset player
</button></div>`).appendTo('#anitracker-modal-body');
$('#anitracker-reset-player').on('click', function() {
closeModal();
resetPlayer();
});
}
$('<span style="display:block;margin-top:10px;">Site:</span>').appendTo('#anitracker-modal-body');
addOptionSwitch('hideThumbnails', 'Hide Thumbnails', 'Hide thumbnails and preview images.');
addOptionSwitch('autoDelete', 'Auto-Clear Links', 'Auto-clearing means only one episode of a series is stored in the tracker at a time.');
addOptionSwitch('autoDownload', 'Automatic Download', 'Automatically download the episode when visiting a download page.');
addOptionSwitch('reduceMotion', 'Reduce Motion', 'Don\'t show animations for opening/closing modal menus.');
openModal();
});
function openShowDataModal() {
$('#anitracker-modal-body').empty();
$(`
<div class="anitracker-modal-list-container">
<div class="anitracker-storage-data" tabindex="0" key="linkList">
<span>Session Data</span>
</div>
</div>
<div class="anitracker-modal-list-container">
<div class="anitracker-storage-data" tabindex="0" key="videoTimes">
<span>Video Progress</span>
</div>
</div>
<div class="anitracker-modal-list-container">
<div class="anitracker-storage-data" tabindex="0" key="videoSpeed">
<span>Video Playback Speed</span>
</div>
</div>
<div class="btn-group">
<button class="btn btn-danger" id="anitracker-reset-data" title="Remove stored data and reset all settings">
<i class="fa fa-undo" aria-hidden="true"></i>
&nbsp;Reset Data
</button>
</div>
<div class="btn-group">
<button class="btn btn-secondary" id="anitracker-raw-data" title="View data in JSON format">
<i class="fa fa-code" aria-hidden="true"></i>
&nbsp;Raw
</button>
</div>
<div class="btn-group">
<button class="btn btn-secondary" id="anitracker-export-data" title="Export and download the JSON data">
<i class="fa fa-download" aria-hidden="true"></i>
&nbsp;Export Data
</button>
</div>
<label class="btn btn-secondary" id="anitracker-import-data-label" tabindex="0" for="anitracker-import-data" style="margin-bottom:0;" title="Import a JSON file with AnimePahe Improvements data. This does not delete any existing data.">
<i class="fa fa-upload" aria-hidden="true"></i>
&nbsp;Import Data
</label>
<div class="btn-group">
<button class="btn btn-dark" id="anitracker-edit-data" title="Edit a key">
<i class="fa fa-pencil" aria-hidden="true"></i>
&nbsp;Edit...
</button>
</div>
<input type="file" id="anitracker-import-data" style="visibility: hidden; width: 0;" accept=".json">
`).appendTo('#anitracker-modal-body');
const expandIcon = `<i class="fa fa-plus anitracker-expand-data-icon" aria-hidden="true"></i>`;
const contractIcon = `<i class="fa fa-minus anitracker-expand-data-icon" aria-hidden="true"></i>`;
$(expandIcon).appendTo('.anitracker-storage-data');
$('.anitracker-storage-data').on('click keydown', (e) => {
if (e.type === 'keydown' && e.key !== "Enter") return;
toggleExpandData($(e.currentTarget));
});
function toggleExpandData(elem) {
if (elem.hasClass('anitracker-expanded')) {
contractData(elem);
}
else {
expandData(elem);
}
}
$('#anitracker-reset-data').on('click', function() {
if (confirm('[AnimePahe Improvements]\n\nThis will remove all saved data and reset it to its default state.\nAre you sure?') === true) {
saveData(getDefaultData());
updatePage();
openShowDataModal();
}
});
$('#anitracker-raw-data').on('click', function() {
const blob = new Blob([JSON.stringify(getStorage())], {type : 'application/json'});
windowOpen(URL.createObjectURL(blob));
});
$('#anitracker-edit-data').on('click', function() {
$('#anitracker-modal-body').empty();
$(`
<b>Warning: for developer use.<br>Back up your data before messing with this.</b>
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-edit-data-key" placeholder="Key (Path)">
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-edit-data-value" placeholder="Value (JSON)">
<p>Leave value empty to get the existing value</p>
<div class="btn-group">
<button class="btn dropdown-toggle btn-secondary anitracker-edit-mode-dropdown-button" data-bs-toggle="dropdown" data-toggle="dropdown" data-value="replace">Replace</button>
<div class="dropdown-menu anitracker-dropdown-content anitracker-edit-mode-dropdown"></div>
</div>
<div class="btn-group">
<button class="btn btn-primary anitracker-confirm-edit-button">Confirm</button>
</div>
`).appendTo('#anitracker-modal-body');
[{t:'Replace',i:'replace'},{t:'Append',i:'append'},{t:'Delete from list',i:'delList'}].forEach(g => { $(`<button ref="${g.i}">${g.t}</button>`).appendTo('.anitracker-edit-mode-dropdown') });
$('.anitracker-edit-mode-dropdown button').on('click', (e) => {
const pressed = $(e.target)
const btn = pressed.parents().eq(1).find('.anitracker-edit-mode-dropdown-button');
btn.data('value', pressed.attr('ref'));
btn.text(pressed.text());
});
$('.anitracker-confirm-edit-button').on('click', () => {
const storage = getStorage();
const key = $('.anitracker-edit-data-key').val();
let keyValue = undefined;
try {
keyValue = eval("storage." + key); // lots of evals here because I'm lazy
}
catch (e) {
console.error(e);
alert("Nope didn't work");
return;
}
if ($('.anitracker-edit-data-value').val() === '') {
alert(JSON.stringify(keyValue));
return;
}
if (keyValue === undefined) {
alert("Undefined");
return;
}
const mode = $('.anitracker-edit-mode-dropdown-button').data('value');
let value = undefined;
if (mode === 'delList') {
value = $('.anitracker-edit-data-value').val();
}
else if ($('.anitracker-edit-data-value').val() !== "undefined") {
try {
value = JSON.parse($('.anitracker-edit-data-value').val());
}
catch (e) {
console.error(e);
alert("Invalid JSON");
return;
}
}
const delFromListMessage = "Please enter a comparison in the 'value' field, with 'a' being the variable for the element.\neg. 'a.id === \"apple\"'\nWhichever elements that match this will be deleted.";
switch (mode) {
case 'replace':
eval(`storage.${key} = value`);
break;
case 'append':
if (keyValue.constructor.name !== 'Array') {
alert("Not a list");
return;
}
eval(`storage.${key}.push(value)`);
break;
case 'delList':
if (keyValue.constructor.name !== 'Array') {
alert("Not a list");
return;
}
try {
eval(`storage.${key} = storage.${key}.filter(a => !(${value}))`);
}
catch (e) {
console.error(e);
alert(delFromListMessage);
return;
}
break;
default:
alert("This message isn't supposed to show up. Uh...");
return;
}
if (JSON.stringify(storage) === JSON.stringify(getStorage())) {
alert("Nothing changed.");
if (mode === 'delList') {
alert(delFromListMessage);
}
return;
}
else alert("Probably worked!");
saveData(storage);
});
openModal(openShowDataModal);
});
$('#anitracker-export-data').on('click', function() {
const storage = getStorage();
if (storage.cache) {
delete storage.cache;
saveData(storage);
}
download('animepahe-tracked-data-' + Date.now() + '.json', JSON.stringify(getStorage(), null, 2));
});
$('#anitracker-import-data-label').on('keydown', (e) => {
if (e.key === "Enter") $("#" + $(e.currentTarget).attr('for')).click();
});
$('#anitracker-import-data').on('change', function(event) {
const file = this.files[0];
const fileReader = new FileReader();
$(fileReader).on('load', function() {
let newData = {};
try {
newData = JSON.parse(fileReader.result);
}
catch (err) {
alert('[AnimePahe Improvements]\n\nPlease input a valid JSON file.');
return;
}
const storage = getStorage();
const diffBefore = importData(storage, newData, false);
let totalChanged = 0;
for (const [key, value] of Object.entries(diffBefore)) {
totalChanged += value;
}
if (totalChanged === 0) {
alert('[AnimePahe Improvements]\n\nThis file contains no changes to import.');
return;
}
$('#anitracker-modal-body').empty();
$(`
<h4>Choose what to import</h4>
<br>
<div class="form-check">
<input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-link-list-check" ${diffBefore.linkListAdded > 0 ? "checked" : "disabled"}>
<label class="form-check-label" for="anitracker-link-list-check">
Session entries (${diffBefore.linkListAdded})
</label>
</div>
<div class="form-check">
<input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-video-times-check" ${(diffBefore.videoTimesAdded + diffBefore.videoTimesUpdated) > 0 ? "checked" : "disabled"}>
<label class="form-check-label" for="anitracker-video-times-check">
Video progress times (${diffBefore.videoTimesAdded + diffBefore.videoTimesUpdated})
</label>
</div>
<div class="form-check">
<input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-bookmarks-check" ${diffBefore.bookmarksAdded > 0 ? "checked" : "disabled"}>
<label class="form-check-label" for="anitracker-bookmarks-check">
Bookmarks (${diffBefore.bookmarksAdded})
</label>
</div>
<div class="form-check">
<input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-notifications-check" ${(diffBefore.notificationsAdded + diffBefore.episodeFeedUpdated) > 0 ? "checked" : "disabled"}>
<label class="form-check-label" for="anitracker-notifications-check">
Episode feed entries (${diffBefore.notificationsAdded})
<ul style="margin-bottom:0;margin-left:-24px;"><li>Episode feed entries updated: ${diffBefore.episodeFeedUpdated}</li></ul>
</label>
</div>
<div class="form-check">
<input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-settings-check" ${diffBefore.settingsUpdated > 0 ? "checked" : "disabled"}>
<label class="form-check-label" for="anitracker-settings-check">
Settings (${diffBefore.settingsUpdated})
</label>
</div>
<div class="btn-group" style="float: right;">
<button class="btn btn-primary" id="anitracker-confirm-import" title="Confirm import">
<i class="fa fa-upload" aria-hidden="true"></i>
&nbsp;Import
</button>
</div>
`).appendTo('#anitracker-modal-body');
$('.anitracker-import-data-input').on('change', (e) => {
let checksOn = 0;
for (const elem of $('.anitracker-import-data-input')) {
if ($(elem).prop('checked')) checksOn++;
}
if (checksOn === 0) {
$('#anitracker-confirm-import').attr('disabled', true);
}
else {
$('#anitracker-confirm-import').attr('disabled', false);
}
});
$('#anitracker-confirm-import').on('click', () => {
const diffAfter = importData(getStorage(), newData, true, {
linkList: !$('#anitracker-link-list-check').prop('checked'),
videoTimes: !$('#anitracker-video-times-check').prop('checked'),
bookmarks: !$('#anitracker-bookmarks-check').prop('checked'),
notifications: !$('#anitracker-notifications-check').prop('checked'),
settings: !$('#anitracker-settings-check').prop('checked')
});
if ((diffAfter.bookmarksAdded + diffAfter.notificationsAdded + diffAfter.settingsUpdated) > 0) updatePage();
if ((diffAfter.videoTimesUpdated + diffAfter.videoTimesAdded) > 0 && isEpisode()) {
sendMessage({action:"change_time", time:getStorage().videoTimes.find(a => a.videoUrls.includes($('.embed-responsive-item')[0].src))?.time});
}
alert('[AnimePahe Improvements]\n\nImported!');
openShowDataModal();
});
openModal(openShowDataModal);
});
fileReader.readAsText(file);
});
function importData(data, importedData, save = true, ignored = {settings:{}}) {
const changed = {
linkListAdded: 0, // Session entries added
videoTimesAdded: 0, // Video progress entries added
videoTimesUpdated: 0, // Video progress times updated
bookmarksAdded: 0, // Bookmarks added
notificationsAdded: 0, // Anime added to episode feed
episodeFeedUpdated: 0, // Episodes either added to episode feed or that had their watched status updated
settingsUpdated: 0 // Settings updated
}
const defaultData = getDefaultData();
if (importedData.version !== defaultData.version) {
upgradeData(importedData, importedData.version);
}
for (const [key, value] of Object.entries(importedData)) {
if (defaultData[key] === undefined) continue;
if (!ignored.linkList && key === 'linkList') {
const added = [];
value.forEach(g => {
if ((g.type === 'episode' && data.linkList.find(h => h.type === 'episode' && h.animeSession === g.animeSession && h.episodeSession === g.episodeSession) === undefined)
|| (g.type === 'anime' && data.linkList.find(h => h.type === 'anime' && h.animeSession === g.animeSession) === undefined)) {
added.push(g);
changed.linkListAdded++;
}
});
data.linkList.splice(0,0,...added);
continue;
}
else if (!ignored.videoTimes && key === 'videoTimes') {
const added = [];
value.forEach(g => {
const foundTime = data.videoTimes.find(h => h.videoUrls.includes(g.videoUrls[0]));
if (foundTime === undefined) {
added.push(g);
changed.videoTimesAdded++;
}
else if (foundTime.time < g.time) {
foundTime.time = g.time;
changed.videoTimesUpdated++;
}
});
data.videoTimes.splice(0,0,...added);
continue;
}
else if (!ignored.bookmarks && key === 'bookmarks') {
value.forEach(g => {
if (data.bookmarks.find(h => h.id === g.id) !== undefined) return;
data.bookmarks.push(g);
changed.bookmarksAdded++;
});
continue;
}
else if (!ignored.notifications && key === 'notifications') {
value.anime.forEach(g => {
if (data.notifications.anime.find(h => h.id === g.id) !== undefined) return;
data.notifications.anime.push(g);
changed.notificationsAdded++;
});
// Checking if there exists any gap between the imported episodes and the existing ones
if (save) data.notifications.anime.forEach(g => {
const existingEpisodes = data.notifications.episodes.filter(a => (a.animeName === g.name || a.animeId === g.id));
const addedEpisodes = value.episodes.filter(a => (a.animeName === g.name || a.animeId === g.id));
if (existingEpisodes.length > 0 && (existingEpisodes[existingEpisodes.length-1].episode - addedEpisodes[0].episode > 1.5)) {
g.updateFrom = new Date(addedEpisodes[0].time + " UTC").getTime();
}
});
value.episodes.forEach(g => {
const anime = (() => {
if (g.animeId !== undefined) return data.notifications.anime.find(a => a.id === g.animeId);
const fromNew = data.notifications.anime.find(a => a.name === g.animeName);
if (fromNew !== undefined) return fromNew;
const id = value.anime.find(a => a.name === g.animeName);
return data.notifications.anime.find(a => a.id === id);
})();
if (anime === undefined) return;
if (g.animeName !== anime.name) g.animeName = anime.name;
if (g.animeId === undefined) g.animeId = anime.id;
const foundEpisode = data.notifications.episodes.find(h => h.animeId === anime.id && h.episode === g.episode);
if (foundEpisode !== undefined) {
if (g.watched === true && !foundEpisode.watched) {
foundEpisode.watched = true;
changed.episodeFeedUpdated++;
}
return;
}
data.notifications.episodes.push(g);
changed.episodeFeedUpdated++;
});
if (save) {
data.notifications.episodes.sort((a,b) => a.time < b.time ? 1 : -1);
if (value.episodes.length > 0) {
data.notifications.lastUpdated = new Date(value.episodes[0].time + " UTC").getTime();
}
}
continue;
}
else if (ignored.settings !== true && key === 'settings') {
for (const [key, value2] of Object.entries(value)) {
if (defaultData.settings[key] === undefined || ignored.settings[key] || ![true,false].includes(value2)) continue;
if (data.settings[key] === value2) continue;
data.settings[key] = value2;
changed.settingsUpdated++;
}
}
}
if (save) saveData(data);
return changed;
}
function getCleanType(type) {
if (type === 'linkList') return "Clean up older duplicate entries";
else if (type === 'videoTimes') return "Remove entries with no progress (0s)";
else return "[Message not found]";
}
function expandData(elem) {
const storage = getStorage();
const dataType = elem.attr('key');
elem.find('.anitracker-expand-data-icon').replaceWith(contractIcon);
const dataEntries = $('<div class="anitracker-modal-list"></div>').appendTo(elem.parent());
const cleanButton = ['linkList','videoTimes'].includes(dataType) ? `<button class="btn btn-secondary anitracker-clean-data-button anitracker-list-btn" style="text-wrap:nowrap;" title="${getCleanType(dataType)}">Clean up</button>` : '';
$(`
<div class="btn-group anitracker-storage-filter">
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-modal-search" placeholder="Search">
<button dir="down" class="btn btn-secondary dropdown-toggle anitracker-reverse-order-button anitracker-list-btn" title="Sort direction (down is default, and means newest first)"></button>
${cleanButton}
</div>
`).appendTo(dataEntries);
elem.parent().find('.anitracker-modal-search').focus();
elem.parent().find('.anitracker-modal-search').on('input', (e) => {
setTimeout(() => {
const query = $(e.target).val();
for (const entry of elem.parent().find('.anitracker-modal-list-entry')) {
if ($($(entry).find('a,span')[0]).text().toLowerCase().includes(query)) {
$(entry).show();
continue;
}
$(entry).hide();
}
}, 10);
});
elem.parent().find('.anitracker-clean-data-button').on('click', () => {
if (!confirm("[AnimePahe Improvements]\n\n" + getCleanType(dataType) + '?')) return;
const updatedStorage = getStorage();
const removed = [];
if (dataType === 'linkList') {
for (let i = 0; i < updatedStorage.linkList.length; i++) {
const link = updatedStorage.linkList[i];
const similar = updatedStorage.linkList.filter(a => a.animeName === link.animeName && a.episodeNum === link.episodeNum);
if (similar[similar.length-1] !== link) {
removed.push(link);
}
}
updatedStorage.linkList = updatedStorage.linkList.filter(a => !removed.includes(a));
}
else if (dataType === 'videoTimes') {
for (const timeEntry of updatedStorage.videoTimes) {
if (timeEntry.time > 5) continue;
removed.push(timeEntry);
}
updatedStorage.videoTimes = updatedStorage.videoTimes.filter(a => !removed.includes(a));
}
alert(`[AnimePahe Improvements]\n\nCleaned up ${removed.length} ${removed.length === 1 ? "entry" : "entries"}.`);
saveData(updatedStorage);
dataEntries.remove();
expandData(elem);
});
// When clicking the reverse order button
elem.parent().find('.anitracker-reverse-order-button').on('click', (e) => {
const btn = $(e.target);
if (btn.attr('dir') === 'down') {
btn.attr('dir', 'up');
btn.addClass('anitracker-up');
}
else {
btn.attr('dir', 'down');
btn.removeClass('anitracker-up');
}
const entries = [];
for (const entry of elem.parent().find('.anitracker-modal-list-entry')) {
entries.push(entry.outerHTML);
}
entries.reverse();
elem.parent().find('.anitracker-modal-list-entry').remove();
for (const entry of entries) {
$(entry).appendTo(elem.parent().find('.anitracker-modal-list'));
}
applyDeleteEvents();
});
function applyDeleteEvents() {
$('.anitracker-modal-list-entry .anitracker-delete-session-button').on('click', function() {
const storage = getStorage();
const href = $(this).parent().find('a').attr('href');
const animeSession = getAnimeSessionFromUrl(href);
if (isEpisode(href)) {
const episodeSession = getEpisodeSessionFromUrl(href);
storage.linkList = storage.linkList.filter(g => !(g.type === 'episode' && g.animeSession === animeSession && g.episodeSession === episodeSession));
saveData(storage);
}
else {
storage.linkList = storage.linkList.filter(g => !(g.type === 'anime' && g.animeSession === animeSession));
saveData(storage);
}
$(this).parent().remove();
});
$('.anitracker-modal-list-entry .anitracker-delete-progress-button').on('click', function() {
const storage = getStorage();
storage.videoTimes = storage.videoTimes.filter(g => !g.videoUrls.includes($(this).attr('lookForUrl')));
saveData(storage);
$(this).parent().remove();
});
$('.anitracker-modal-list-entry .anitracker-delete-speed-entry-button').on('click', function() {
const storage = getStorage();
storage.videoSpeed = storage.videoSpeed.filter(g => g.animeId !== parseInt($(this).attr('animeId')));
saveData(storage);
$(this).parent().remove();
});
}
if (dataType === 'linkList') {
[...storage.linkList].reverse().forEach(g => {
const name = g.animeName + (g.type === 'episode' ? (' - Episode ' + g.episodeNum) : '');
$(`
<div class="anitracker-modal-list-entry">
<a target="_blank" href="/${(g.type === 'episode' ? 'play/' : 'anime/') + g.animeSession + (g.type === 'episode' ? ('/' + g.episodeSession) : '')}" title="${toHtmlCodes(name)}">
${name}
</a><br>
<button class="btn btn-danger anitracker-delete-session-button anitracker-flat-button" title="Delete this stored session">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Delete
</button>
</div>`).appendTo(elem.parent().find('.anitracker-modal-list'));
});
applyDeleteEvents();
}
else if (dataType === 'videoTimes') {
[...storage.videoTimes].reverse().forEach(g => {
$(`
<div class="anitracker-modal-list-entry">
<span>
${g.animeName} - Episode ${g.episodeNum}
</span><br>
<span>
Current time: ${secondsToHMS(g.time)}
</span><br>
<button class="btn btn-danger anitracker-delete-progress-button anitracker-flat-button" lookForUrl="${g.videoUrls[0]}" title="Delete this video progress">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Delete
</button>
</div>`).appendTo(elem.parent().find('.anitracker-modal-list'));
});
applyDeleteEvents();
}
else if (dataType === 'videoSpeed') {
[...storage.videoSpeed].reverse().forEach(g => {
$(`
<div class="anitracker-modal-list-entry">
<span>
${g.animeName}
</span><br>
<span>
Playback speed: ${g.speed}
</span><br>
<button class="btn btn-danger anitracker-delete-speed-entry-button anitracker-flat-button" animeId="${g.animeId}" title="Delete this video speed entry">
<i class="fa fa-trash" aria-hidden="true"></i>
&nbsp;Delete
</button>
</div>`).appendTo(elem.parent().find('.anitracker-modal-list'));
});
applyDeleteEvents();
}
elem.addClass('anitracker-expanded');
}
function contractData(elem) {
elem.find('.anitracker-expand-data-icon').replaceWith(expandIcon);
elem.parent().find('.anitracker-modal-list').remove();
elem.removeClass('anitracker-expanded');
elem.blur();
}
openModal();
}
$('#anitracker-show-data').on('click', openShowDataModal);
}
addGeneralButtons();
if (isEpisode()) {
$(`
<span style="margin-left: 30px;"><i class="fa fa-files-o" aria-hidden="true"></i>&nbsp;Copy:</span>
<div class="btn-group">
<button class="btn btn-dark anitracker-copy-button" copy="link" data-placement="top" data-content="Copied!">Link</button>
</div>
<div class="btn-group" style="margin-right:30px;">
<button class="btn btn-dark anitracker-copy-button" copy="link-time" data-placement="top" data-content="Copied!">Link & Time</button>
</div>`).appendTo('#anitracker');
addOptionSwitch('autoPlayNext','Auto-Play Next','Automatically go to the next episode when the current one has ended.','#anitracker');
$('.anitracker-copy-button').on('click', (e) => {
const targ = $(e.currentTarget);
const type = targ.attr('copy');
const name = encodeURIComponent(getAnimeName());
const episode = getEpisodeNum();
if (['link','link-time'].includes(type)) {
navigator.clipboard.writeText(window.location.origin + '/customlink?a=' + name + '&e=' + episode + (type !== 'link-time' ? '' : ('&t=' + currentEpisodeTime.toString())));
}
targ.popover('show');
setTimeout(() => {
targ.popover('hide');
}, 1000);
});
}
if (initialStorage.settings.autoDelete === true && isEpisode() && paramArray.find(a => a[0] === 'ref' && a[1] === 'customlink') === undefined) {
const animeData = getAnimeData();
deleteEpisodesFromTracker(getEpisodeNum(), animeData.title, animeData.id);
}
function updateSwitches() {
const storage = getStorage();
for (const s of optionSwitches) {
const different = s.value !== storage.settings[s.optionId];
if (!different) continue;
s.value = storage.settings[s.optionId];
$(`#anitracker-${s.switchId}-switch`).prop('checked', s.value === true);
if (s.value === true) {
if (s.onEvent !== undefined) s.onEvent();
}
else if (s.offEvent !== undefined) {
s.offEvent();
}
}
}
updateSwitches();
function addOptionSwitch(optionId, name, desc = '', parent = '#anitracker-modal-body') {
const option = optionSwitches.find(s => s.optionId === optionId);
$(`
<div class="custom-control custom-switch anitracker-switch" id="anitracker-${option.switchId}" title="${desc}">
<input type="checkbox" class="custom-control-input" id="anitracker-${option.switchId}-switch">
<label class="custom-control-label" for="anitracker-${option.switchId}-switch">${name}</label>
</div>`).appendTo(parent);
const switc = $(`#anitracker-${option.switchId}-switch`);
switc.prop('checked', option.value);
const events = [option.onEvent, option.offEvent];
switc.on('change', (e) => {
const checked = $(e.currentTarget).is(':checked');
const storage = getStorage();
if (checked !== storage.settings[optionId]) {
storage.settings[optionId] = checked;
option.value = checked;
saveData(storage);
}
if (checked) {
if (events[0] !== undefined) events[0]();
}
else if (events[1] !== undefined) events[1]();
});
}
$(`
<div class="anitracker-download-spinner" style="display: none;">
<div class="spinner-border text-danger" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>`).prependTo('#downloadMenu,#episodeMenu');
$('.prequel img,.sequel img').attr('loading','');
}
@CallMeTurk
Copy link

  • @CallMeTurk I'm working on seeing how possible skipping intros is. It's difficult, but I've made some progress on it.

Super! Keep up the good work and take your time!

@Ellivers
Copy link
Author

Ellivers commented Jan 9, 2025

4.0.0 now supports skipping episode sections, but doesn't work for all anime yet. I'll add section data for all anime eventually, but right now it mainly supports old anime and popular anime.
Data is taken from a fork I made of an existing project to make a database for anime timestamps: https://github.com/Ellivers/open-anime-timestamps/

@CallMeTurk

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment