Last active
November 17, 2024 12:57
-
-
Save Ellivers/f7716b6b6895802058c367963f3a2c51 to your computer and use it in GitHub Desktop.
AnimePahe Improvements
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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 3.18 | |
// @author Ellivers | |
// @description 2022-06-06, 18:04:32 | |
// ==/UserScript== | |
/* | |
How to install: | |
* Get the Violentmonkey browser extension and then click the "Raw" button on this page | |
* 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. | |
* The saved data for old sessions can be cleared and is fully viewable and editable. | |
* 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). | |
* 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 and search query. | |
* Automatically finds a relevant cover for the top of anime pages. | |
* 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 gradually. | |
* 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 "Autoplay" 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 { | |
linkList:[], | |
videoTimes:[], | |
bookmarks:[], | |
notifications: { | |
lastUpdated: Date.now(), | |
anime: [], | |
episodes: [] | |
}, | |
badCovers: [], | |
autoDelete:true, | |
hideThumbnails:false, | |
theatreMode:false, | |
bestQuality:true, | |
autoDownload:true, | |
autoPlay:false | |
}; | |
} | |
function getStorage() { | |
const defa = getDefaultData(); | |
const res = GM_getValue('anime-link-tracker', defa); | |
for (const key of Object.keys(defa)) { | |
if (res[key] !== undefined) continue; | |
res[key] = defa[key]; | |
} | |
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) { | |
return storage.videoTimes.find(a => a.animeName === name && a.episodeNum === ep); | |
} | |
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.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; | |
} | |
if ($('.anitracker-message').length > 0) { | |
console.log("[AnimePahe Improvements (Player)] Script was reloaded."); | |
return; | |
} | |
$(` | |
<div class="anitracker-loading plyr__control--overlaid" style="opacity: 1; border-radius: 10%;"> | |
<span>Loading...</span> | |
</div>`).appendTo('.plyr--video'); | |
$('button.plyr__controls__item:nth-child(1)').hide(); | |
$('.plyr__progress__container').hide(); | |
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] | |
}; | |
} | |
function updateTime() { | |
const currentTime = player.currentTime; | |
const storage = getStorage(); | |
// Delete the storage entry | |
if (player.duration - currentTime <= 20) { | |
const videoInfo = getVideoInfo(); | |
for (const videoData of storage.videoTimes) { | |
if (videoData.animeName === videoInfo.animeName && videoData.episodeNum === videoInfo.episodeNum) { | |
const index = storage.videoTimes.indexOf(videoData); | |
storage.videoTimes.splice(index, 1); | |
} | |
} | |
saveData(storage); | |
return; | |
} | |
const vidInfo = getVideoInfo(); | |
const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage); | |
if (storedVideoTime === undefined) { | |
const vidInfo = getVideoInfo(); | |
storage.videoTimes.push({ | |
videoUrls: [url], | |
time: player.currentTime, | |
animeName: vidInfo.animeName, | |
episodeNum: vidInfo.episodeNum | |
}); | |
if (storage.videoTimes.length > 1000) { | |
storage.splice(0,1); | |
} | |
saveData(storage); | |
return; | |
} | |
storedVideoTime.time = player.currentTime; | |
if (storedVideoTime.playbackRate !== undefined || player.playbackRate !== 1) storedVideoTime.playbackRate = player.playbackRate; | |
saveData(storage); | |
} | |
if (initialStorage.videoTimes === undefined) { | |
initialStorage.videoTimes = []; | |
saveData(initialStorage); | |
} | |
// Messages received from main page | |
window.onmessage = function(e) { | |
const data = e.data; | |
const action = data.action; | |
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 | |
}); | |
} | |
} | |
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.videoUrls.includes(url)) { | |
storedVideoTime.videoUrls.push(url); | |
saveData(storage); | |
} | |
if (![undefined,1].includes(storedVideoTime.playbackRate)) { | |
setSpeed(storedVideoTime.playbackRate); | |
} | |
} | |
else { | |
storage.videoTimes.push({ | |
videoUrls: [url], | |
time: 0, | |
animeName: getVideoInfo().animeName, | |
episodeNum: getVideoInfo().episodeNum | |
}); | |
if (storage.videoTimes.length > 1000) { | |
storage.splice(0,1); | |
} | |
saveData(storage); | |
removeLoadingIndicators(); | |
} | |
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); | |
}); | |
function removeLoadingIndicators() { | |
$('.anitracker-loading').remove(); | |
$('button.plyr__controls__item:nth-child(1)').show(); | |
$('.plyr__progress__container').show(); | |
} | |
player.addEventListener('timeupdate', function() { | |
if (player.currentTime % 10 < 0.5) { | |
updateTime(); | |
} | |
}); | |
player.addEventListener('pause', () => { | |
if (player.readyState <= 2) return; | |
updateTime(); | |
}); | |
player.addEventListener('seeked', () => { | |
updateTime(); | |
removeLoadingIndicators(); | |
}); | |
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)) { | |
if (player.loop) { | |
showMessage('Loop: Off'); | |
} | |
else { | |
showMessage('Loop: On'); | |
} | |
player.loop = !player.loop; | |
return; | |
} | |
if (e.shiftKey && ['n','N'].includes(key)) { | |
window.top.postMessage({ | |
action: "next" | |
}, '*'); | |
return; | |
} | |
if (e.shiftKey && ['p','P'].includes(key)) { | |
window.top.postMessage({ | |
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 = ""; | |
} | |
window.top.postMessage({ | |
action: "key", | |
key: key | |
}, '*'); | |
}); | |
// Ctrl+scrolling to change speed | |
$(` | |
<div class="anitracker-message" style="width:50%;height:10%;position:absolute;background-color:rgba(0,0,0,0.5);display:none;justify-content:center;align-items:center;margin-top:1.5%;border-radius:20px;"> | |
<span style="color: white;font-size: 2.5em;">2.0x</span> | |
</div>`).appendTo($(player).parent().parent()); | |
jQuery.event.special.wheel = { | |
setup: function( _, ns, handle ){ | |
this.addEventListener("wheel", handle, { passive: false }); | |
} | |
}; | |
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; | |
})(); | |
const defaultSpeeds = player.plyr.options.speed; | |
function changeSpeed(e, delta) { | |
if (!e.ctrlKey) return; | |
if (delta == 0) return; | |
const speedChange = e.shiftKey ? 0.05 : 0.1; | |
setSpeed(player.playbackRate + speedChange * (delta > 0 ? -1 : 1)); | |
e.preventDefault(); | |
} | |
function setSpeed(speed) { | |
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; | |
} | |
console.log('[AnimePahe Improvements]', initialStorage); | |
if (initialStorage.hideThumbnails === true) { | |
hideThumbnails(); | |
} | |
function windowOpen(url, target = '_blank') { | |
$(`<a href="${url}" target="${target}"></a>`)[0].click(); | |
} | |
// -------- AnimePahe Improvements CSS --------- | |
$('style').remove(); // Removes a pre-existing style sheet that only has 2 rules | |
$("head").append('<style id="anitracker-style" type="text/css"></style>'); | |
const sheet = $("#anitracker-style")[0].sheet; | |
const animationTimes = { | |
modalOpen: 0.2, | |
fadeIn: 0.2 | |
} | |
const rules = ` | |
#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: 666; | |
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:999; | |
} | |
#anitracker-modal-close { | |
font-size: 2.5em; | |
margin: 3px 10px; | |
cursor: pointer; | |
height: 1em; | |
} | |
#anitracker-modal-close:hover { | |
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 img {display: none;\n} | |
.anitracker-hide.anitracker-thumbnail { | |
border: 10px solid rgb(32, 32, 32); | |
aspect-ratio: 16/9; | |
} | |
.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 { | |
width: 24px; | |
height: 24px; | |
float: right; | |
margin-top: 6px; | |
margin-right: 8px; | |
} | |
.anitracker-modal-list-container { | |
background-color: rgb(40,40,40); | |
margin-bottom: 10px; | |
border-radius: 12px; | |
} | |
.anitracker-storage-data { | |
background-color: var(--gray); | |
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 h4 {display:inline-block;\n} | |
.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-modal-list-entry button { | |
padding-top: 0; | |
padding-bottom: 0; | |
} | |
.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-items-box { | |
width: 150px; | |
display: inline-block; | |
} | |
.anitracker-items-box > div { | |
height:45px; | |
width:100%; | |
border-bottom: 2px solid #454d54; | |
} | |
.anitracker-items-box > button { | |
background: none; | |
border: 1px solid #ccc; | |
color: white; | |
padding: 0; | |
margin-left: 110px; | |
vertical-align: bottom; | |
border-radius: 5px; | |
line-height: 1em; | |
width: 2.5em; | |
font-size: .8em; | |
padding-bottom: .1em; | |
margin-bottom: 2px; | |
} | |
.anitracker-items-box > button:hover { | |
background: #ccc; | |
color: black; | |
} | |
.anitracker-items-box-search { | |
position: absolute; | |
max-width: 150px; | |
max-height: 45px; | |
min-width: 150px; | |
min-height: 45px; | |
overflow-wrap: break-word; | |
overflow-y: auto; | |
} | |
.anitracker-items-box .placeholder { | |
color: #999; | |
position: absolute; | |
z-index: -999; | |
} | |
.anitracker-filter-icon { | |
padding: 2px; | |
background-color: #d5015b; | |
border-radius: 5px; | |
display: inline-block; | |
cursor: pointer; | |
} | |
.anitracker-filter-icon:hover { | |
border: 1px solid white; | |
} | |
.anitracker-text-input { | |
display: inline-block; | |
height: 1em; | |
} | |
.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-reverse-order-button { | |
height: 42px; | |
border-radius: 7px!important; | |
font-size: 2em; | |
color: #ddd!important; | |
margin-left: 10px!important; | |
} | |
.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 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; | |
} | |
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; | |
opacity: .8; | |
color: #ff006c!important; | |
font-size: 2rem; | |
vertical-align: middle; | |
cursor: pointer; | |
padding: 0; | |
box-shadow: none!important; | |
} | |
.anitracker-title-icon:hover { | |
opacity: 1; | |
} | |
.anitracker-title-icon-check { | |
color: white; | |
margin-left: -.7rem; | |
font-size: 1rem; | |
vertical-align: super; | |
text-shadow: none; | |
} | |
.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; | |
} | |
@media screen and (min-width: 1375px) { | |
.anitracker-theatre-mode { | |
max-width: 80%!important; | |
} | |
} | |
@keyframes anitracker-modalOpen { | |
0% { | |
transform: scale(0.5); | |
} | |
20% { | |
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); | |
} | |
} | |
`.split(/^\}/mg).map(a => a.replace(/\n/gm,'') + '}'); | |
for (let i = 0; i < rules.length - 1; i++) { | |
sheet.insertRule(rules[i], i); | |
} | |
const optionSwitches = [ | |
{ | |
optionId: 'autoDelete', | |
switchId: 'auto-delete', | |
value: initialStorage.autoDelete | |
}, | |
{ | |
optionId: 'theatreMode', | |
switchId: 'theatre-mode', | |
value: initialStorage.theatreMode, | |
onEvent: () => { | |
theatreMode(true); | |
}, | |
offEvent: () => { | |
theatreMode(false); | |
} | |
}, | |
{ | |
optionId: 'hideThumbnails', | |
switchId: 'hide-thumbnails', | |
value: initialStorage.hideThumbnails, | |
onEvent: hideThumbnails, | |
offEvent: () => { | |
$('.anitracker-hide').removeClass('anitracker-hide'); | |
$('#anitracker-hide-style').remove(); | |
} | |
}, | |
{ | |
optionId: 'bestQuality', | |
switchId: 'best-quality', | |
value: initialStorage.bestQuality, | |
onEvent: bestVideoQuality | |
}, | |
{ | |
optionId: 'autoDownload', | |
switchId: 'auto-download', | |
value: initialStorage.autoDownload | |
}, | |
{ | |
optionId: 'autoPlay', | |
switchId: 'autoplay', | |
value: initialStorage.autoPlay | |
}]; | |
// Things that update when focusing this tab | |
$(document).on('visibilitychange', () => { | |
if (document.hidden) return; | |
updateSwitches(); | |
const storage = getStorage(); | |
const data = (url.includes('/anime/') || isEpisode()) ? getAnimeData() : undefined; | |
if (data !== undefined) { | |
if (storage.bookmarks.find(a => a.id === data.id) === undefined) { | |
$('.anitracker-bookmark-toggle .anitracker-title-icon-check').hide(); | |
} | |
if (storage.notifications.anime.find(a => a.id === data.id) === undefined) { | |
$('.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.animeName === $(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.removeClass('fa-eye'); | |
eye.addClass('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 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', () => { | |
modalCloseFunction(); | |
}); | |
} | |
addModal(); | |
function openModal(closeFunction = closeModal) { | |
if (closeFunction !== closeModal) { | |
$('#anitracker-modal-close').removeClass('fa-close'); | |
$('#anitracker-modal-close').addClass('fa-arrow-left'); | |
} | |
else { | |
$('#anitracker-modal-close').removeClass('fa-arrow-left'); | |
$('#anitracker-modal-close').addClass('fa-close'); | |
} | |
return new Promise(resolve => { | |
playAnimation($('#anitracker-modal-content'), 'modalOpen'); | |
playAnimation($('#anitracker-modal'), 'fadeIn').then(() => { | |
$('#anitracker-modal').focus(); | |
resolve(); | |
}); | |
$('#anitracker-modal').css('display','flex'); | |
modalCloseFunction = closeFunction; | |
}); | |
} | |
function closeModal() { | |
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 === 'key') { | |
if (data.key === 't') { | |
toggleTheatreMode(); | |
} | |
} | |
else if (data === 'ended') { | |
const storage = getStorage(); | |
if (storage.autoPlay !== 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) { | |
$('.embed-responsive-item')[0].contentWindow.postMessage(message,'*'); | |
} | |
function toggleTheatreMode() { | |
const storage = getStorage(); | |
theatreMode(!storage.theatreMode); | |
storage.theatreMode = !storage.theatreMode; | |
saveData(storage); | |
updateSwitches(); | |
} | |
function getSeasonValue(season) { | |
return ({spring:0, summer:1, fall:2, winter:3})[season.toLowerCase()]; | |
} | |
function getSeasonName(season) { | |
return ["spring","summer","fall","winter"][season]; | |
} | |
function stringSimilarity(s1, s2) { | |
var longer = s1; | |
var shorter = s2; | |
if (s1.length < s2.length) { | |
longer = s2; | |
shorter = s1; | |
} | |
var 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(); | |
var costs = new Array(); | |
for (var i = 0; i <= s1.length; i++) { | |
var lastValue = i; | |
for (var j = 0; j <= s2.length; j++) { | |
if (i == 0) | |
costs[j] = j; | |
else { | |
if (j > 0) { | |
var 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(`season/${getSeasonName(d)}-${i.toString()}`); | |
} | |
} | |
return filters; | |
} | |
const is404 = $('h1').text().includes('404'); | |
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"} | |
], | |
"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"} | |
], | |
"":[ | |
{"value":"airing"},{"value":"completed"} | |
] | |
}; | |
const filterRules = { | |
genre: "and", | |
theme: "and", | |
demographic: "or", | |
type: "or", | |
season: "or", | |
"": "or" | |
}; | |
function getFilterParts(filter) { | |
const regex = /^(?:([\w\-]+)(?:\/))?([\w\-\.]+)$/.exec(filter); | |
return { | |
type: regex[1] || '', | |
value: regex[2] | |
}; | |
} | |
function buildFilterString(type, value) { | |
return (type === '' ? type : type + '/') + value; | |
} | |
const seasonFilterRegex = /^season\/(spring|summer|winter|fall)-\d{4}\.\.(spring|summer|winter|fall)-\d{4}$/; | |
const noneFilterRegex = /^([\w\d\-]+\/)?none$/; | |
function getFilteredList(filtersInput, filterTotal = 0) { | |
let filterNum = 0; | |
function getPage(pageUrl) { | |
return new Promise((resolve, reject) => { | |
const cached = filterSearchCache[pageUrl]; | |
if (cached !== undefined) { | |
if (cached === 'invalid') { | |
resolve(undefined); | |
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) { | |
resolve(undefined); | |
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 = getFilterParts(filter).type; | |
if (noneFilterRegex.test(filter)) { | |
getLists(filterValues[filterType].map(a => buildFilterString(filterType, a.value))).then((filtered) => { | |
getPage('/anime').then((unfiltered) => { | |
const none = []; | |
for (const entry of unfiltered) { | |
if (filtered.find(list => list.entries.find(a => a.name === entry.name)) !== undefined) continue; | |
none.push(entry); | |
} | |
lists.push({ | |
type: filterType, | |
entries: none | |
}); | |
check(); | |
}); | |
}); | |
return; | |
} | |
getPage('/anime/' + filter).then((result) => { | |
if (result !== undefined) { | |
lists.push({ | |
type: filterType, | |
entries: result | |
}); | |
} | |
if (filterTotal > 0) { | |
filterNum++; | |
$($('.anitracker-filter-spinner>span')[0]).text(Math.floor((filterNum/filterTotal) * 100).toString() + '%'); | |
} | |
check(); | |
}); | |
} | |
check(); | |
}); | |
} | |
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; | |
} | |
const seasonFilter = filters.find(a => seasonFilterRegex.test(a)); | |
if (seasonFilter !== undefined) { | |
filters.splice(filters.indexOf(seasonFilter), 1); | |
const range = getFilterParts(seasonFilter).value.split('..'); | |
filters.push(...getSeasonTimeframe({ | |
year: +range[0].split('-')[1], | |
season: getSeasonValue(range[0].split('-')[0]) | |
}, | |
{ | |
year: +range[1].split('-')[1], | |
season: getSeasonValue(range[1].split('-')[0]) | |
})); | |
} | |
getLists(filters).then((listsInput) => { | |
const lists = JSON.parse(JSON.stringify(listsInput)); | |
const types = {}; | |
for (const list of lists) { | |
if (types[list.type]) continue; | |
types[list.type] = list.entries; | |
} | |
lists.splice(0, 1); | |
for (const list of lists) { | |
const entries = list.entries; | |
if (filterRules[list.type] === 'and') { | |
const matches = []; | |
for (const anime of types[list.type]) { | |
if (entries.find(a => a.name === anime.name) === undefined) continue; | |
matches.push(anime); | |
} | |
types[list.type] = matches; | |
} | |
else if (filterRules[list.type] === 'or') { | |
for (const anime of list.entries) { | |
if (types[list.type].find(a => a.name === anime.name) !== undefined) continue; | |
types[list.type].push(anime); | |
} | |
} | |
} | |
const listOfTypes = Array.from(Object.values(types)); | |
let finalList = listOfTypes[0]; | |
listOfTypes.splice(0,1); | |
for (const type of listOfTypes) { | |
const matches = []; | |
for (const anime of type) { | |
if (finalList.find(a => a.name === anime.name) === undefined) continue; | |
matches.push(anime); | |
} | |
finalList = matches; | |
} | |
resolve(finalList); | |
}); | |
}); | |
} | |
function searchList(fuseClass, list, query, limit = 80) { | |
const fuse = new fuseClass(list, { | |
keys: ['name'] | |
}); | |
const matching = fuse.search(query); | |
const matches = []; | |
for (let i = 0; i < matching.length; i++) { | |
if (i >= limit) break; | |
const match = matching[i]; | |
matches.push(match.item); | |
} | |
return matches; | |
} | |
function timeSince(date) { | |
var seconds = Math.floor((new Date() - date) / 1000); | |
var 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() && e.key === 'Escape') { | |
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;"> (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, """).replace(/'/g, "'"); | |
} | |
// 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> | |
<button class="btn btn-danger" title="Remove this anime from the episode feed"> | |
<i class="fa fa-trash" aria-hidden="true"></i> | |
Remove | |
</button> | |
</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 button').on('click', (e) => { | |
const name = $(e.currentTarget).parent().attr('animename'); | |
toggleNotifications(name); | |
const name2 = getAnimeName(); | |
if (name2.length > 0 && name2 === name) { | |
$('.anitracker-notifications-toggle .anitracker-title-icon-check').hide(); | |
} | |
$(e.currentTarget).parent().remove(); | |
}); | |
openModal(openNotificationsModal); | |
}); | |
const animeData = {}; | |
const queue = [...oldStorage.notifications.anime]; | |
openModal().then(() => { | |
next(); | |
}); | |
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[anime.name] = 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) { | |
console.log(anime.latest_episode); | |
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); | |
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[ep.animeName]; | |
if (data === undefined) return; | |
$(` | |
<div class="anitracker-big-list-item anitracker-notification-item${ep.watched ? "" : " anitracker-notification-item-unwatched"} anitracker-temp" anime-data="${toHtmlCodes(data.title)}" 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(new Date(ep.time + " UTC"))} ago</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.animeName === parent.attr('anime-data') && a.episode === +parent.attr('episode-data')); | |
if (ep === undefined) return; | |
if (parent.hasClass('anitracker-notification-item-unwatched')) { | |
parent.removeClass('anitracker-notification-item-unwatched'); | |
elem.removeClass('fa-eye'); | |
elem.addClass('fa-eye-slash'); | |
} | |
else { | |
parent.addClass('anitracker-notification-item-unwatched'); | |
elem.addClass('fa-eye'); | |
elem.removeClass('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" 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" title="Remove this bookmark"> | |
<i class="fa fa-trash" aria-hidden="true"></i> | |
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) { | |
const storage = getStorage(); | |
const found = 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 !== name); | |
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); | |
if (data === undefined) return -1; | |
const episodes = await asyncGetResponseData('/api?m=release&sort=episode_desc&id=' + data.session); | |
if (episodes === undefined) return 0; | |
return new Promise((resolve, reject) => { | |
if (episodes.length === 0) resolve(undefined); | |
for (const anime of storage.notifications.anime) { | |
if (anime.name !== animeName) continue; | |
anime.latest_episode = episodes[0].created_at; | |
break; | |
} | |
for (const ep of episodes) { | |
const found = storage.notifications.episodes.find(a => a.episode === ep.episode && a.animeName === animeName); | |
if (found !== undefined) { | |
found.session = ep.session; | |
continue; | |
} | |
if (new Date(ep.created_at + " UTC").getTime() < storage.notifications.lastUpdated) { | |
continue; | |
} | |
storage.notifications.episodes.push({ | |
session: ep.session, | |
animeName: animeName, | |
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); | |
}); | |
} | |
// Search/index page | |
if (/^\/anime\/?$/.test(window.location.pathname)) { | |
$(` | |
<div id="anitracker" class="anitracker-index" style="margin-bottom: 10px;"> | |
<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> | |
Random Anime | |
</button> | |
<div class="anitracker-items-box" id="anitracker-genre-list" dropdown="genre"> | |
<button default="and" title="Toggle filter logic">and</button> | |
<div> | |
<div class="anitracker-items-box-search" contenteditable="" spellcheck="false"><span class="anitracker-text-input"></span></div> | |
<span class="placeholder">Genre</span> | |
</div> | |
</div> | |
<div class="anitracker-items-box" id="anitracker-theme-list" dropdown="theme"> | |
<button default="and" title="Toggle filter logic">and</button> | |
<div> | |
<div class="anitracker-items-box-search" contenteditable="" spellcheck="false"><span class="anitracker-text-input"></span></div> | |
<span class="placeholder">Theme</span> | |
</div> | |
</div> | |
<div class="anitracker-items-box" id="anitracker-type-list" dropdown="type"> | |
<div> | |
<div class="anitracker-items-box-search" contenteditable="" spellcheck="false"><span class="anitracker-text-input"></span></div> | |
<span class="placeholder">Type (or)</span> | |
</div> | |
</div> | |
<div class="anitracker-items-box" id="anitracker-demographic-list" dropdown="demographic"> | |
<div> | |
<div class="anitracker-items-box-search" contenteditable="" spellcheck="false"><span class="anitracker-text-input"></span></div> | |
<span class="placeholder">Demographic (or)</span> | |
</div> | |
</div> | |
<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" id="Layer_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>`).insertBefore('.index'); | |
$('.anitracker-items-box-search').on('focus click', (e) => { | |
showDropdown(e.currentTarget); | |
}); | |
function showDropdown(elem) { | |
$('.anitracker-dropdown-content').css('display', ''); | |
const dropdown = $(`#anitracker-${$(elem).closest('.anitracker-items-box').attr('dropdown')}-dropdown`); | |
dropdown.show(); | |
dropdown.css('position', 'absolute'); | |
const pos = $(elem).closest('.anitracker-items-box-search').position(); | |
dropdown.css('left', pos.left); | |
dropdown.css('top', pos.top + 40); | |
} | |
$('.anitracker-items-box-search').on('blur', (e) => { | |
setTimeout(() => { | |
const dropdown = $(`#anitracker-${$(e.target).parents().eq(1).attr('dropdown')}-dropdown`); | |
if (dropdown.is(':active') || dropdown.is(':focus')) return; | |
dropdown.hide(); | |
}, 10); | |
}); | |
$('.anitracker-items-box-search').on('keydown', (e) => { | |
setTimeout(() => { | |
const targ =$(e.target); | |
const type = targ.parents().eq(1).attr('dropdown'); | |
const dropdown = $(`#anitracker-${type}-dropdown`); | |
for (const icon of targ.find('.anitracker-filter-icon')) { | |
(() => { | |
if ($(icon).text() === $(icon).data('name')) return; | |
const filter = $(icon).data('filter'); | |
$(icon).remove(); | |
for (const active of dropdown.find('.anitracker-active')) { | |
if ($(active).attr('ref') !== filter) continue; | |
removeFilter(filter, targ, $(active)); | |
return; | |
} | |
removeFilter(filter, targ, undefined); | |
})(); | |
} | |
if (dropdown.find('.anitracker-active').length > targ.find('.anitracker-filter-icon').length) { | |
const filters = []; | |
for (const icon of targ.find('.anitracker-filter-icon')) { | |
filters.push($(icon).data('filter')); | |
} | |
let removedFilter = false; | |
for (const active of dropdown.find('.anitracker-active')) { | |
if (filters.includes($(active).attr('ref'))) continue; | |
removedFilter = true; | |
removeFilter($(active).attr('ref'), targ, $(active), false); | |
} | |
if (removedFilter) refreshSearchPage(appliedFilters); | |
} | |
for (const filter of appliedFilters) { // Special case for non-default filters | |
(() => { | |
const parts = getFilterParts(filter); | |
if (parts.type !== type || filterValues[parts.type].includes(parts.value)) return; | |
for (const icon of targ.find('.anitracker-filter-icon')) { | |
if ($(icon).data('filter') === filter) return; | |
} | |
appliedFilters.splice(appliedFilters.indexOf(filter), 1); | |
refreshSearchPage(appliedFilters); | |
})(); | |
} | |
targ.find('br').remove(); | |
updateFilterBox(targ[0]); | |
}, 10); | |
}); | |
function setIconEvent(elem) { | |
$(elem).on('click', (e) => { | |
const targ = $(e.target); | |
for (const btn of $(`#anitracker-${targ.closest('.anitracker-items-box').attr('dropdown')}-dropdown button`)) { | |
if ($(btn).attr('ref') !== targ.data('filter')) continue; | |
removeFilter(targ.data('filter'), targ.parent(), btn); | |
return; | |
} | |
removeFilter(targ.data('filter'), targ.parent(), undefined); | |
}); | |
} | |
function updateFilterBox(elem) { | |
const targ = $(elem); | |
for (const icon of targ.find('.anitracker-filter-icon')) { | |
if (appliedFilters.includes($(icon).data('filter'))) continue; | |
$(icon).remove(); | |
} | |
if (appliedFilters.length === 0) { | |
for (const input of targ.find('.anitracker-text-input')) { | |
if ($(input).text().trim() !== '') continue; | |
$(input).text(''); | |
} | |
} | |
const text = getFilterBoxText(targ[0]).trim(); | |
const dropdownBtns = $(`#anitracker-${targ.parents().eq(1).attr('dropdown')}-dropdown button`); | |
dropdownBtns.show(); | |
if (text !== '') { | |
for (const btn of dropdownBtns) { | |
if ($(btn).text().toLowerCase().includes(text.toLowerCase())) continue; | |
$(btn).hide(); | |
} | |
} | |
if (targ.text().trim() === '') { | |
targ.text(''); | |
targ.parent().find('.placeholder').show(); | |
return; | |
} | |
targ.parent().find('.placeholder').hide(); | |
} | |
function getFilterBoxText(elem) { | |
const basicText = $(elem).contents().filter(function(){return this.nodeType == Node.TEXT_NODE;})[0] | |
const spanText = $($(elem).find('.anitracker-text-input')[$(elem).find('.anitracker-text-input').length-1]).text() + ''; | |
if (basicText === undefined) return spanText; | |
return (basicText.nodeValue + spanText).trim(); | |
} | |
$('.anitracker-items-box>button').on('click', (e) => { | |
const targ = $(e.target); | |
const newRule = targ.text() === 'and' ? 'or' : 'and'; | |
const type = targ.parent().attr('dropdown'); | |
filterRules[type] = newRule; | |
targ.text(newRule); | |
const filterBox = targ.parent().find('.anitracker-items-box-search'); | |
filterBox.focus(); | |
const filterList = appliedFilters.filter(a => a.startsWith(type + '/')); | |
if (newRule === 'and' && filterList.length > 1 && filterList.find(a => a.startsWith(type + '/none')) !== undefined) { | |
for (const btn of $(`#anitracker-${type}-dropdown button`)) { | |
if ($(btn).attr('ref') !== type + '/none' ) continue; | |
removeFilter(type + '/none', filterBox, btn, false); | |
break; | |
} | |
} | |
if (filterList.length > 0) refreshSearchPage(appliedFilters); | |
}); | |
const animeList = getAnimeList(); | |
$(` | |
<span style="display: block;margin-bottom: 10px;font-size: 1.2em;color:#ddd;" id="anitracker-filter-result-count">Filter results: <span>${animeList.length}</span></span> | |
`).insertAfter('#anitracker'); | |
$('#anitracker-random-anime').on('click', function() { | |
const storage = getStorage(); | |
storage.cache = filterSearchCache; | |
saveData(storage); | |
const params = getParams(appliedFilters, $('.anitracker-items-box>button')); | |
if ($('#anitracker-anime-list-search').length > 0 && $('#anitracker-anime-list-search').val() !== '') { | |
$.getScript('https://cdn.jsdelivr.net/npm/[email protected]', function() { | |
const query = $('#anitracker-anime-list-search').val(); | |
getRandomAnime(searchList(Fuse, animeList, query), (params === '' ? '?anitracker-random=1' : params + '&anitracker-random=1') + '&search=' + encodeURIComponent(query)); | |
}); | |
} | |
else { | |
getRandomAnime(animeList, params === '' ? '?anitracker-random=1' : params + '&anitracker-random=1'); | |
} | |
}); | |
function getDropdownButtons(filters, type) { | |
return filters.sort((a,b) => a.name > b.name ? 1 : -1).map(g => $(`<button ref="${type}/${g.value}">${g.name}</button>`)); | |
} | |
$(`<div id="anitracker-genre-dropdown" dropdown="genre" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-genre-list'); | |
getDropdownButtons(filterValues.genre, 'genre').forEach(g => { g.appendTo('#anitracker-genre-dropdown') }); | |
$(`<button ref="genre/none">(None)</button>`).appendTo('#anitracker-genre-dropdown'); | |
$(`<div id="anitracker-theme-dropdown" dropdown="theme" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-theme-list'); | |
getDropdownButtons(filterValues.theme, 'theme').forEach(g => { g.appendTo('#anitracker-theme-dropdown') }); | |
$(`<button ref="theme/none">(None)</button>`).appendTo('#anitracker-theme-dropdown'); | |
$(`<div id="anitracker-type-dropdown" dropdown="type" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-type-list'); | |
getDropdownButtons(filterValues.type, 'type').forEach(g => { g.appendTo('#anitracker-type-dropdown') }); | |
$(`<button ref="type/none">(None)</button>`).appendTo('#anitracker-type-dropdown'); | |
$(`<div id="anitracker-demographic-dropdown" dropdown="demographic" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-demographic-list'); | |
getDropdownButtons(filterValues.demographic, 'demographic').forEach(g => { g.appendTo('#anitracker-demographic-dropdown') }); | |
$(`<button ref="demographic/none">(None)</button>`).appendTo('#anitracker-demographic-dropdown'); | |
$(`<div id="anitracker-status-dropdown" dropdown="status" class="dropdown-menu anitracker-dropdown-content">`).insertAfter('#anitracker-status-button'); | |
['all','airing','completed'].forEach(g => { $(`<button ref="${g}">${g[0].toUpperCase() + g.slice(1)}</button>`).appendTo('#anitracker-status-dropdown') }); | |
$(`<button ref="none">(No status)</button>`).appendTo('#anitracker-status-dropdown'); | |
const timeframeSettings = { | |
enabled: false | |
}; | |
$('#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">Enable</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> | |
</div> | |
<div class="btn-group"> | |
<button class="btn btn-secondary" id="anitracker-season-copy-to-lower" style="color:white;margin-left:14px;" 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"><i class="fa fa-check" aria-hidden="true"></i> Done</button> | |
</div> | |
</div>`).appendTo('#anitracker-modal-body'); | |
$('.anitracker-year-input').val(new Date().getFullYear()); | |
$('#anitracker-settings-enable-switch').on('change', () => { | |
updateDisabled($('#anitracker-settings-enable-switch').is(':checked')); | |
}); | |
$('#anitracker-settings-enable-switch').prop('checked', timeframeSettings.enabled); | |
updateDisabled(timeframeSettings.enabled); | |
function updateDisabled(enabled) { | |
$('.anitracker-season-group').find('input,button').prop('disabled', !enabled); | |
} | |
$('#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'); | |
['Spring','Summer','Fall','Winter'].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.parent().parent().find('.anitracker-season-dropdown-button'); | |
btn.data('value', pressed.text()); | |
btn.text(pressed.text()); | |
}); | |
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(); | |
} | |
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(); | |
} | |
$('#anitracker-modal-confirm-button').on('click', () => { | |
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 ($('#anitracker-settings-enable-switch').is(':checked')) { | |
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.'); | |
return; | |
} | |
if (to.year - from.year > 100) { | |
alert('[AnimePahe Improvements]\n\nYear interval cannot be more than 100 years.'); | |
return; | |
} | |
removeSeasonsFromFilters(); | |
appliedFilters.push(`season/${getSeasonName(from.season)}-${from.year.toString()}..${getSeasonName(to.season)}-${to.year.toString()}`); | |
$('#anitracker-time-search-button').addClass('anitracker-active'); | |
} | |
else { | |
removeSeasonsFromFilters(); | |
$('#anitracker-time-search-button').removeClass('anitracker-active'); | |
} | |
timeframeSettings.enabled = $('#anitracker-settings-enable-switch').is(':checked'); | |
timeframeSettings.from = from; | |
timeframeSettings.to = to; | |
closeModal(); | |
refreshSearchPage(appliedFilters, true); | |
}); | |
openModal(); | |
}); | |
function removeSeasonsFromFilters() { | |
const newFilters = []; | |
for (const filter of appliedFilters) { | |
if (filter.startsWith('season/')) continue; | |
newFilters.push(filter); | |
} | |
appliedFilters.length = 0; | |
appliedFilters.push(...newFilters); | |
} | |
const appliedFilters = []; | |
$('.anitracker-items-dropdown').on('click', (e) => { | |
const filterSearchBox = $(`#anitracker-${/^anitracker-([^\-]+)-dropdown$/.exec($(e.target).closest('.anitracker-dropdown-content').attr('id'))[1]}-list .anitracker-items-box-search`); | |
filterSearchBox.focus(); | |
if (!$(e.target).is('button')) return; | |
const filter = $(e.target).attr('ref'); | |
if (appliedFilters.includes(filter)) { | |
removeFilter(filter, filterSearchBox, e.target); | |
} | |
else { | |
addFilter(filter, filterSearchBox, e.target); | |
} | |
}); | |
$('#anitracker-status-dropdown').on('click', (e) => { | |
if (!$(e.target).is('button')) return; | |
const filter = $(e.target).attr('ref'); | |
addStatusFilter(filter); | |
refreshSearchPage(appliedFilters); | |
}); | |
function addStatusFilter(filter) { | |
if (appliedFilters.includes(filter)) return; | |
for (const btn of $('#anitracker-status-dropdown button')) { | |
if ($(btn).attr('ref') !== filter) continue; | |
$('#anitracker-status-button').text($(btn).text()); | |
} | |
if (filter !== 'all') $('#anitracker-status-button').addClass('anitracker-active'); | |
else $('#anitracker-status-button').removeClass('anitracker-active'); | |
for (const filter2 of appliedFilters) { | |
if (filter2.includes('/')) continue; | |
appliedFilters.splice(appliedFilters.indexOf(filter2), 1); | |
} | |
if (filter !== 'all') appliedFilters.push(filter); | |
} | |
function addFilter(name, filterBox, filterButton, refreshPage = true) { | |
const filterType = getFilterParts(name).type; | |
if (filterType !== '' && filterRules[filterType] === 'and') { | |
if (name.endsWith('/none')) { | |
for (const filter of appliedFilters.filter(a => a.startsWith(filterType))) { | |
if (filter.endsWith('/none')) continue; | |
removeFilter(filter, filterBox, (() => { | |
for (const btn of $(filterButton).parent().find('button')) { | |
if ($(btn).attr('ref') !== filter) continue; | |
return btn; | |
} | |
})(), false); | |
} | |
} | |
else if (appliedFilters.includes(filterType + '/none')) { | |
removeFilter(filterType + '/none', filterBox, (() => { | |
for (const btn of $(filterButton).parent().find('button')) { | |
if ($(btn).attr('ref') !== filterType + '/none') continue; | |
return btn; | |
} | |
})(), false); | |
} | |
} | |
$(filterBox).find('.anitracker-text-input').text(''); | |
const basicText = $(filterBox).contents().filter(function(){return this.nodeType == Node.TEXT_NODE;})[0]; | |
if (basicText !== undefined) basicText.nodeValue = ''; | |
addFilterIcon($(filterBox)[0], name, $(filterButton).text()); | |
$(filterButton).addClass('anitracker-active'); | |
appliedFilters.push(name); | |
if (refreshPage) refreshSearchPage(appliedFilters); | |
updateFilterBox(filterBox); | |
} | |
function removeFilter(name, filterBox, filterButton, refreshPage = true) { | |
$(filterBox).find('.anitracker-text-input').text(''); | |
const basicText = $(filterBox).contents().filter(function(){return this.nodeType == Node.TEXT_NODE;})[0]; | |
if (basicText !== undefined) basicText.nodeValue = ''; | |
removeFilterIcon($(filterBox)[0], name); | |
$(filterButton).removeClass('anitracker-active'); | |
appliedFilters.splice(appliedFilters.indexOf(name), 1); | |
if (refreshPage) refreshSearchPage(appliedFilters); | |
updateFilterBox(filterBox); | |
} | |
function addFilterIcon(elem, filter, nameInput) { | |
const name = nameInput || getFilterParts(filter).value; | |
setIconEvent($(` | |
<span class="anitracker-filter-icon" data-name="${name}" data-filter="${filter}">${name}</span><span class="anitracker-text-input"> </span> | |
`).after(' ').appendTo(elem)); | |
} | |
function removeFilterIcon(elem, name) { | |
for (const f of $(elem).find('.anitracker-filter-icon')) { | |
if ($(f).text() === name) $(f).remove(); | |
} | |
} | |
const searchQueue = []; | |
function refreshSearchPage(filtersInput, screenSpinner = false, fromQueue = false) { | |
const filters = JSON.parse(JSON.stringify(filtersInput)); | |
if (!fromQueue) { | |
if (screenSpinner) { | |
$(` | |
<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;" class="anitracker-filter-spinner"> | |
<div class="spinner-border" role="status" style="color:#d5015b;width:5rem;height:5rem;"> | |
<span class="sr-only">Loading...</span> | |
</div> | |
<span style="position: absolute;font-weight: bold;">0%</span> | |
</div>`).prependTo(document.body); | |
} | |
else { | |
$(` | |
<div style="display: inline-flex;margin-left: 10px;justify-content: center;align-items: center;vertical-align: bottom;" class="anitracker-filter-spinner"> | |
<div class="spinner-border" role="status" style="color:#d5015b;"> | |
<span class="sr-only">Loading...</span> | |
</div> | |
<span style="position: absolute;font-size: .5em;font-weight: bold;">0%</span> | |
</div>`).appendTo('.page-index h1'); | |
} | |
searchQueue.push(filters); | |
if (searchQueue.length > 1) return; | |
} | |
if (filters.length === 0) { | |
updateFilterResults([], true).then(() => { | |
animeList.length = 0; | |
animeList.push(...getAnimeList()); | |
$('#anitracker-filter-result-count span').text(animeList.length.toString()); | |
$($('.anitracker-filter-spinner')[0]).remove(); | |
searchQueue.shift(); | |
if (searchQueue.length > 0) { | |
refreshSearchPage(searchQueue[0], screenSpinner, true); | |
return; | |
} | |
if ($('#anitracker-anime-list-search').val() === '') return; | |
$('#anitracker-anime-list-search').trigger('anitracker:search'); | |
}); | |
return; | |
} | |
let filterTotal = 0; | |
for (const filter of filters) { | |
const parts = getFilterParts(filter); | |
if (noneFilterRegex.test(filter)) { | |
filterTotal += filterValues[parts.type].length; | |
continue; | |
} | |
if (seasonFilterRegex.test(filter)) { | |
const range = parts.value.split('..'); | |
filterTotal += getSeasonTimeframe({ | |
year: +range[0].split('-')[1], | |
season: getSeasonValue(range[0].split('-')[0]) | |
}, | |
{ | |
year: +range[1].split('-')[1], | |
season: getSeasonValue(range[1].split('-')[0]) | |
}).length; | |
continue; | |
} | |
filterTotal++; | |
} | |
getFilteredList(filters, filterTotal).then((finalList) => { | |
if (finalList === undefined) { | |
alert('[AnimePahe Improvements]\n\nSearch filter failed.'); | |
$($('.anitracker-filter-spinner')[0]).remove(); | |
searchQueue.length = 0; | |
refreshSearchPage([]); | |
return; | |
} | |
finalList.sort((a,b) => a.name > b.name ? 1 : -1); | |
updateFilterResults(finalList).then(() => { | |
animeList.length = 0; | |
animeList.push(...finalList); | |
$($('.anitracker-filter-spinner')[0]).remove(); | |
updateParams(appliedFilters, $('.anitracker-items-box>button')); | |
searchQueue.shift(); | |
if (searchQueue.length > 0) { | |
refreshSearchPage(searchQueue[0], screenSpinner, true); | |
return; | |
} | |
if ($('#anitracker-anime-list-search').val() === '') return; | |
$('#anitracker-anime-list-search').trigger('anitracker:search'); | |
}); | |
}); | |
} | |
function updateFilterResults(list, noFilters = false) { | |
return new Promise((resolve, reject) => { | |
$('.anitracker-filter-result').remove(); | |
$('#anitracker-filter-results').remove(); | |
$('.nav-item').show(); | |
if (noFilters) { | |
$('.index>').show(); | |
$('.index>>>>div').show(); | |
updateParams(appliedFilters); | |
resolve(); | |
return; | |
} | |
$('#anitracker-filter-result-count span').text(list.length.toString()); | |
$('.index>>>>div').hide(); | |
if (list.length >= 100) { | |
$('.index>').show(); | |
list.forEach(anime => { | |
const elem = $(` | |
<div class="anitracker-filter-result col-12 col-md-6"> | |
${anime.html} | |
</div>`); | |
const matchLetter = (() => { | |
if (/^[A-Za-z]/.test(anime.name)) { | |
return anime.name[0].toUpperCase(); | |
} | |
else { | |
return 'hash' | |
} | |
})(); | |
for (const tab of $('.tab-content').children()) { | |
if (tab.id !== matchLetter) continue; | |
elem.appendTo($(tab).children()[0]); | |
} | |
}); | |
for (const tab of $('.tab-content').children()) { | |
if ($(tab).find('.anitracker-filter-result').length > 0) continue; | |
const tabId = $(tab).attr('id'); | |
for (const navLink of $('.nav-link')) { | |
if (($(navLink).attr('role') !== 'tab' || $(navLink).text() !== tabId) && !($(navLink).text() === '#' && tabId === 'hash')) continue; | |
$(navLink).parent().hide(); | |
} | |
} | |
if ($('.nav-link.active').parent().css('display') === 'none') { | |
let visibleTabs = 0; | |
for (const navLink of $('.nav-link')) { | |
if ($(navLink).parent().css('display') === 'none' || $(navLink).text().length > 1) continue; | |
visibleTabs++; | |
} | |
for (const navLink of $('.nav-link')) { | |
if ($(navLink).parent().css('display') === 'none' || $(navLink).text().length > 1) continue; | |
if ($(navLink).text() === "#" && visibleTabs > 1) continue; | |
$(navLink).click(); | |
break; | |
} | |
} | |
} | |
else { | |
$('.index>').hide(); | |
$(`<div class="row" id="anitracker-filter-results"></div>`).prependTo('.index'); | |
let matches = ''; | |
list.forEach(anime => { | |
matches += ` | |
<div class="col-12 col-md-6"> | |
${anime.html} | |
</div>`; | |
}); | |
if (list.length === 0) matches = `<div class="col-12 col-md-6">No results found.</div>`; | |
$(matches).appendTo('#anitracker-filter-results'); | |
} | |
resolve(); | |
}); | |
} | |
function updateParams(filters, ruleButtons = []) { | |
window.history.replaceState({}, document.title, "/anime" + getParams(filters, ruleButtons)); | |
} | |
function getParams(filters, ruleButtons = []) { | |
const filterArgs = textFromFilterList(filters); | |
let params = (filterArgs.length > 0 ? ('?' + filterArgs) : ''); | |
if (ruleButtons.length > 0) { | |
for (const btn of ruleButtons) { | |
if ($(btn).text() === $(btn).attr('default')) continue; | |
params += '&' + $(btn).parent().attr('dropdown') + '-rule=' + $(btn).text(); | |
} | |
} | |
return params; | |
} | |
$.getScript('https://cdn.jsdelivr.net/npm/[email protected]', function() { | |
$(` | |
<div class="btn-group"> | |
<input id="anitracker-anime-list-search" autocomplete="off" class="form-control anitracker-text-input-bar" style="width: 150px;" placeholder="Search"> | |
</div>`).appendTo('#anitracker'); | |
let typingTimer; | |
$('#anitracker-anime-list-search').on('anitracker:search', function() { | |
animeListSearch(); | |
}); | |
$('#anitracker-anime-list-search').on('keyup', function() { | |
clearTimeout(typingTimer); | |
typingTimer = setTimeout(animeListSearch, 150); | |
}); | |
$('#anitracker-anime-list-search').on('keydown', function() { | |
clearTimeout(typingTimer); | |
}); | |
function animeListSearch() { | |
$('#anitracker-search-results').remove(); | |
const value = $('#anitracker-anime-list-search').val(); | |
if (value === '') { | |
$('.index>').show(); | |
if (animeList.length < 100) $('.scrollable-ul').hide(); | |
const newSearchParams = new URLSearchParams(window.location.search); | |
newSearchParams.delete('search'); | |
window.history.replaceState({}, document.title, "/anime" + (Array.from(newSearchParams.entries()).length > 0 ? ('?' + newSearchParams.toString()) : '')); | |
} | |
else { | |
$('.index>').hide(); | |
const matches = searchList(Fuse, animeList, value); | |
$(`<div class="row" id="anitracker-search-results"></div>`).prependTo('.index'); | |
let elements = ''; | |
matches.forEach(match => { | |
elements += ` | |
<div class="col-12 col-md-6"> | |
${match.html} | |
</div>`; | |
}); | |
if (matches.length === 0) elements = `<div class="col-12 col-md-6">No results found.</div>` | |
$(elements).appendTo('#anitracker-search-results'); | |
const newSearchParams = new URLSearchParams(window.location.search); | |
newSearchParams.set('search', value); | |
window.history.replaceState({}, document.title, "/anime?" + newSearchParams.toString()); | |
} | |
} | |
const searchParams = new URLSearchParams(window.location.search); | |
if (searchParams.has('search')) { | |
$('#anitracker-anime-list-search').val(searchParams.get('search')); | |
animeListSearch(); | |
} | |
}).fail(() => { | |
$(` | |
<div class="btn-group"> | |
<span>Fuse.js failed to load.</span> | |
</div>`).appendTo('#anitracker'); | |
}); | |
const urlFilters = filterListFromParams(new URLSearchParams(window.location.search)); | |
for (const filter of urlFilters) { | |
const parts = getFilterParts(filter); | |
const type = parts.type; | |
if (type === '') { | |
addStatusFilter(filter); | |
continue; | |
} | |
const searchBox = $(`#anitracker-${type}-list .anitracker-items-box-search`); | |
const dropdown = Array.from($(`#anitracker-${type}-dropdown`).children()).find(a=> $(a).attr('ref') === filter); | |
if (type.endsWith('-rule')) { | |
for (const btn of $('.anitracker-items-box>button')) { | |
const type2 = $(btn).parent().attr('dropdown'); | |
if (type2 !== type.split('-')[0]) continue; | |
$(btn).text(parts.value); | |
} | |
continue; | |
} | |
if (type === 'season') { | |
if (!seasonFilterRegex.test(filter)) continue; | |
appliedFilters.push(filter); | |
$('#anitracker-time-search-button').addClass('anitracker-active'); | |
const range = parts.value.split('..'); | |
timeframeSettings.enabled = true; | |
timeframeSettings.from = { | |
year: +range[0].split('-')[1], | |
season: getSeasonValue(range[0].split('-')[0]) | |
}; | |
timeframeSettings.to = { | |
year: +range[1].split('-')[1], | |
season: getSeasonValue(range[1].split('-')[0]) | |
}; | |
continue; | |
} | |
if (searchBox.length === 0) { | |
appliedFilters.push(filter); | |
continue; | |
} | |
addFilter(filter, searchBox, dropdown, false); | |
continue; | |
} | |
if (urlFilters.length > 0) refreshSearchPage(appliedFilters, true); | |
return; | |
} | |
function filterListFromParams(params, allowRules = true) { | |
const filters = []; | |
for (const [key, values] of params.entries()) { | |
const key2 = (key === 'other' ? '' : key); | |
if (!filterRules[key2] && !key.endsWith('-rule')) continue; | |
if (key.endsWith('-rule')) { | |
filterRules[key.split('-')[0]] = values === 'and' ? 'and' : 'or'; | |
if (!allowRules) continue; | |
} | |
decodeURIComponent(values).split(',').forEach(value => { | |
filters.push((key2 === '' ? '' : key2 + '/') + value); | |
}); | |
} | |
return filters; | |
} | |
function textFromFilterList(filters) { | |
const filterTypes = {}; | |
filters.forEach(filter => { | |
const parts = getFilterParts(filter); | |
let key = (() => { | |
if (parts.type === '') return 'other'; | |
return parts.type; | |
})(); | |
if (filterTypes[key] === undefined) filterTypes[key] = []; | |
filterTypes[key].push(parts.value); | |
}); | |
const finishedList = []; | |
for (const [key, values] of Object.entries(filterTypes)) { | |
finishedList.push(key + '=' + encodeURIComponent(values.join(','))); | |
} | |
return finishedList.join('&'); | |
} | |
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($) { | |
$.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()); | |
}); | |
}; | |
})(jQuery); | |
function isEpisode(url = window.location.toString()) { | |
return url.includes('/play/'); | |
} | |
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) { | |
const storage = getStorage(); | |
const animeName = nameInput || getAnimeName(); | |
const linkData = getStoredLinkData(storage); | |
for (const episode of storage.linkList) { | |
if (episode.type === 'episode' && episode.animeName === animeName && episode.episodeNum !== exclude) { | |
const index = storage.linkList.indexOf(episode); | |
storage.linkList.splice(index, 1); | |
} | |
} | |
for (const timeData of storage.videoTimes) { | |
if (timeData.episodeNum !== exclude && stringSimilarity(timeData.animeName, animeName) > 0.81) { | |
const index = storage.videoTimes.indexOf(timeData); | |
storage.videoTimes.splice(index, 1); | |
} | |
} | |
saveData(storage); | |
} | |
function deleteEpisodeFromTracker(animeName, episodeNum) { | |
const storage = getStorage(); | |
storage.linkList = storage.linkList.filter(a => !(a.type == 'episode' && a.animeName == animeName && a.episodeNum == episodeNum)); | |
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\.]+) Online/.exec($('.theatre-info h1').text())[1] : $($('.title-wrapper h1 span')[0]).text(); | |
} | |
function getEpisodeNum() { | |
if (isEpisode()) return +(/Watch (.*) - ([\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 getAnimeData(name = getAnimeName()) { | |
if (name.length === 0) return undefined; | |
const response = getResponseData('/api?m=search&q=' + encodeURIComponent(name)); | |
if (response === undefined) return response; | |
for (const anime of response) { | |
if (anime.title === name) { | |
return anime; | |
} | |
} | |
return undefined; | |
} | |
async function asyncGetAnimeData(name = getAnimeName()) { | |
const response = await getResponseData('/api?m=search&q=' + encodeURIComponent(name)); | |
return new Promise((resolve, reject) => { | |
if (response === undefined) resolve(response); | |
for (const anime of response) { | |
if (anime.title === name) { | |
resolve(anime); | |
} | |
} | |
reject(undefined); | |
}); | |
} | |
// 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: | |
* 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); | |
if (animeData === undefined) return 2; | |
const episodeSession = getEpisodeSession(animeData.session, episodeNum); | |
if (episodeSession === undefined) return 3; | |
if (bobj !== undefined) { | |
for (const g of storage.linkList) { | |
if (g.type === 'episode' && g.animeSession === bobj.animeSession && g.episodeSession === bobj.episodeSession) { | |
storage.linkList.splice(storage.linkList.indexOf(g), 1); | |
} | |
} | |
} | |
saveData(storage); | |
window.location.replace('/play/' + animeData.session + '/' + episodeSession + window.location.search); | |
return 0; | |
} | |
else if (bobj !== undefined && bobj.animeId !== undefined) { | |
for (const g of storage.linkList) { | |
if (g.type === 'anime' && g.animeSession === bobj.animeSession) { | |
storage.linkList.splice(storage.linkList.indexOf(g), 1); | |
} | |
} | |
saveData(storage); | |
window.location.replace('/a/' + bobj.animeId); | |
return 0; | |
} | |
else { | |
let animeData = getAnimeData(name); | |
if (animeData === undefined) return 2; | |
window.location.replace('/a/' + animeData.id); | |
return 0; | |
} | |
return 4; | |
} | |
const obj = getStoredLinkData(initialStorage); | |
if (isEpisode() && !is404) $('#downloadMenu').changeElementType('button'); | |
console.log('[AnimePahe Improvements]', obj, animeSession, episodeSession); | |
function setSessionData() { | |
const animeName = getAnimeName(); | |
const storage = getStorage(); | |
if (isEpisode()) { | |
storage.linkList.push({ | |
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.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'); | |
} | |
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; | |
} | |
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.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 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.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) { | |
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> | |
Refresh Session | |
</button> | |
</div>`); | |
} | |
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]; | |
request.open('GET', '/api?m=release&id=' + relationSession, false); | |
request.send(); | |
if (request.status !== 200) return undefined; | |
const episodeList = []; | |
const response = JSON.parse(request.response); | |
for (let i = 1; i <= response.last_page; i++) { | |
const episodes = getResponseData(`/api?m=release&sort=episode_asc&page=${i}&id=${relationSession}`); | |
if (episodes !== undefined) { | |
[].push.apply(episodeList, episodes); | |
} | |
} | |
if (episodeList.length === 0) return undefined; | |
return { | |
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'); | |
const relationData = getRelationData(animeData.session, relationType); | |
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 = 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"> | |
<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().autoDelete === true) { | |
deleteEpisodesFromTracker(undefined, relationData.name); | |
} | |
} | |
async function setSequelPoster() { | |
const relationData = 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"> | |
<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> Refreshing...', timeout); | |
} | |
else if (result === 2 || result === 3) { | |
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> | |
Clear from Tracker | |
</button>`).appendTo('#anitracker'); | |
$('#anitracker-clear-from-tracker').on('click', function() { | |
const animeName = getAnimeName(); | |
if (isEpisode()) { | |
deleteEpisodeFromTracker(animeName, getEpisodeNum()); | |
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)`); | |
} | |
if (!isEpisode()) { | |
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 ($('.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> | |
Clear Episodes from Tracker | |
</button>`).appendTo('#anitracker'); | |
$('#anitracker-clear-episodes-from-tracker').on('click', function() { | |
deleteEpisodesFromTracker(); | |
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> 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> Save Session</button> | |
</div> | |
</div>`).appendTo('.title-wrapper'); | |
$('#anitracker-reroll-button').on('click', function() { | |
$(this).text('Rerolling...'); | |
const sourceFilters = new URLSearchParams(sourceParams.toString()); | |
getFilteredList(filterListFromParams(sourceFilters, false)).then((animeList) => { | |
storage.cache = filterSearchCache; | |
saveData(storage); | |
if (sourceParams.has('search')) { | |
$.getScript('https://cdn.jsdelivr.net/npm/[email protected]', function() { | |
getRandomAnime(searchList(Fuse, animeList, decodeURIComponent(sourceParams.get('search'))), '?' + sourceParams.toString(), '_self'); | |
}); | |
} | |
else { | |
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"); | |
$(` | |
<span style="margin-left:5%;pointer-events:auto;" title="${date.toDateString() + " " + date.toLocaleTimeString()}">${date.toLocaleDateString()}</span> | |
`).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.name === animename) !== 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)) { | |
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', | |
...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 hideAnimePageThumbnails() { | |
$("head").append('<style id="anitracker-hide-style" type="text/css"></style>'); | |
let sheet = $("#anitracker-hide-style")[0].sheet; | |
sheet.insertRule(` | |
.episode-snapshot img { | |
display: none; | |
}`, 0); | |
sheet.insertRule(` | |
.episode-snapshot { | |
border: 4px solid var(--dark); | |
}`, 1); | |
} | |
function hideThumbnails() { | |
if (isEpisode()) { | |
$('.anitracker-thumbnail').addClass('anitracker-hide'); | |
} | |
else { | |
hideAnimePageThumbnails(); | |
} | |
} | |
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> | |
Manage Data... | |
</button> | |
<button class="btn btn-dark" id="anitracker-settings" title="Settings"> | |
<i class="fa fa-sliders" aria-hidden="true"></i> | |
Settings... | |
</button>`).appendTo('#anitracker'); | |
$('#anitracker-settings').on('click', () => { | |
$('#anitracker-modal-body').empty(); | |
addOptionSwitch('auto-delete', 'Auto-Clear Links', 'Auto-clearing means only one episode of a series is stored in the tracker at a time.', 'autoDelete'); | |
addOptionSwitch('theatre-mode', 'Theatre Mode', 'Expand the video player for a better experience on bigger screens.', 'theatreMode'); | |
addOptionSwitch('hide-thumbnails', 'Hide Thumbnails', 'Hide thumbnails and preview images.', 'hideThumbnails'); | |
addOptionSwitch('best-quality', 'Default to Best Quality', 'Automatically select the best resolution quality available.', 'bestQuality'); | |
addOptionSwitch('auto-download', 'Automatic Download', 'Automatically download the episode when visiting a download page.', 'autoDownload'); | |
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> | |
Reset player | |
</button></div>`).appendTo('#anitracker-modal-body'); | |
$('#anitracker-reset-player').on('click', function() { | |
closeModal(); | |
setIframeUrl(stripUrl($('.embed-responsive-item').attr('src'))); | |
}); | |
} | |
openModal(); | |
}); | |
function openShowDataModal() { | |
$('#anitracker-modal-body').empty(); | |
$(` | |
<div class="anitracker-modal-list-container"> | |
<div class="anitracker-storage-data" tabindex="0" key="linkList"> | |
<h4>Session Data</h4> | |
</div> | |
<div class="anitracker-modal-list"></div> | |
</div> | |
<div class="anitracker-modal-list-container"> | |
<div class="anitracker-storage-data" tabindex="0" key="videoTimes"> | |
<h4>Video Progress</h4> | |
</div> | |
<div class="anitracker-modal-list"></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> | |
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> | |
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> | |
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> | |
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> | |
Edit... | |
</button> | |
</div> | |
<input type="file" id="anitracker-import-data" style="visibility: hidden; width: 0;" accept=".json"> | |
`).appendTo('#anitracker-modal-body'); | |
const expandIcon = `<svg fill="#ffffff" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 455 455" xml:space="preserve" class="anitracker-expand-data-icon" aria-hidden="true"> | |
<polygon points="455,212.5 242.5,212.5 242.5,0 212.5,0 212.5,212.5 0,212.5 0,242.5 212.5,242.5 212.5,455 242.5,455 242.5,242.5 455,242.5 "/> | |
</svg>`; | |
const contractIcon = `<svg fill="#ffffff" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800px" height="800px" viewBox="0 0 83 83" xml:space="preserve" class="anitracker-expand-data-icon" aria-hidden="true"> | |
<path d="M81,36.166H2c-1.104,0-2,0.896-2,2v6.668c0,1.104,0.896,2,2,2h79c1.104,0,2-0.896,2-2v-6.668 C83,37.062,82.104,36.166,81,36.166z"/> | |
</svg>`; | |
$(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()); | |
closeModal(); | |
} | |
}); | |
$('#anitracker-raw-data').on('click', function() { | |
const blob = new Blob([JSON.stringify(getStorage(), null, 2)], {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.parent().parent().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 { | |
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 === \"banana\"'\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); | |
openShowDataModal(); | |
}); | |
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 > 0 ? "checked" : "disabled"}> | |
<label class="form-check-label" for="anitracker-notifications-check"> | |
Episode feed entries (${diffBefore.notificationsAdded}) | |
</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> | |
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.settingsUpdated > 0) updateSwitches(); | |
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 | |
episodeFeedAdded: 0, // Episodes added to episode feed | |
settingsUpdated: 0 // Settings updated | |
} | |
for (const [key, value] of Object.entries(importedData)) { | |
if (getDefaultData()[key] === undefined || ignored.settings[key]) continue; | |
if (!ignored.linkList && key === 'linkList') { | |
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)) { | |
data.linkList.push(g); | |
changed.linkListAdded++; | |
} | |
}); | |
continue; | |
} | |
else if (!ignored.videoTimes && key === 'videoTimes') { | |
value.forEach(g => { | |
const foundTime = data.videoTimes.find(h => h.videoUrls.includes(g.videoUrls[0])); | |
if (foundTime === undefined) { | |
data.videoTimes.push(g); | |
changed.videoTimesAdded++; | |
} | |
else if (foundTime.time < g.time) { | |
foundTime.time = g.time; | |
changed.videoTimesUpdated++; | |
} | |
}); | |
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++; | |
}); | |
value.episodes.forEach(g => { | |
if (data.notifications.episodes.find(h => h.animeName === g.animeName && h.episode === g.episode) !== undefined) return; | |
data.notifications.episodes.push(g); | |
changed.episodeFeedAdded++; | |
}); | |
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; | |
} | |
if ((value !== true && value !== false) || data[key] === undefined || data[key] === value || ignored.settings === true) continue; | |
data[key] = value; | |
changed.settingsUpdated++; | |
} | |
if (save) saveData(data); | |
return changed; | |
} | |
function expandData(elem) { | |
const storage = getStorage(); | |
elem.find('.anitracker-expand-data-icon').replaceWith(contractIcon); | |
const dataEntries = elem.parent().find('.anitracker-modal-list'); | |
$(` | |
<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" title="Sort direction (down is default, and means newest first)"></button> | |
</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); | |
}); | |
// 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(); | |
}); | |
} | |
if (elem.attr('key') === '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" title="Delete this stored session"> | |
<i class="fa fa-trash" aria-hidden="true"></i> | |
Delete | |
</button> | |
</div>`).appendTo(elem.parent().find('.anitracker-modal-list')); | |
}); | |
applyDeleteEvents(); | |
} | |
else if (elem.attr('key') === '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" lookForUrl="${g.videoUrls[0]}" title="Delete this video progress"> | |
<i class="fa fa-trash" aria-hidden="true"></i> | |
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').empty(); | |
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> 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('autoplay','Auto-play','Automatically go to the next episode when the current one has ended.','autoPlay','#anitracker'); | |
$('.anitracker-copy-button').on('click', (e) => { | |
const targ = $(e.currentTarget); | |
const type = targ.attr('copy'); | |
const name = encodeURIComponent(getAnimeName()); | |
const episode = getEpisodeNum(); | |
if (type === 'link') { | |
navigator.clipboard.writeText(window.location.origin + '/customlink?a=' + name + '&e=' + episode); | |
} | |
else if (type === 'link-time') { | |
navigator.clipboard.writeText(window.location.origin + '/customlink?a=' + name + '&e=' + episode + '&t=' + currentEpisodeTime.toString()); | |
} | |
targ.popover('show'); | |
setTimeout(() => { | |
targ.popover('hide'); | |
}, 1000); | |
}); | |
} | |
if (initialStorage.autoDelete === true && isEpisode() && paramArray.find(a => a[0] === 'ref' && a[1] === 'customlink') === undefined) { | |
deleteEpisodesFromTracker(getEpisodeNum()); | |
} | |
function updateSwitches() { | |
const storage = getStorage(); | |
for (const s of optionSwitches) { | |
if (s.value !== storage[s.optionId]) { | |
s.value = storage[s.optionId]; | |
} | |
if (s.value === true) { | |
if (s.onEvent !== undefined) s.onEvent(); | |
} | |
else if (s.offEvent !== undefined) { | |
s.offEvent(); | |
} | |
} | |
optionSwitches.forEach(s => { | |
$(`#anitracker-${s.switchId}-switch`).prop('checked', storage[s.optionId] === true); | |
$(`#anitracker-${s.switchId}-switch`).change(); | |
}); | |
} | |
updateSwitches(); | |
function addOptionSwitch(id, name, desc = '', optionId, parent = '#anitracker-modal-body') { | |
const option = (() => { | |
for (const s of optionSwitches) { | |
if (s.optionId !== optionId) continue; | |
return s; | |
} | |
})(); | |
$(` | |
<div class="custom-control custom-switch anitracker-switch" id="anitracker-${id}" title="${desc}"> | |
<input type="checkbox" class="custom-control-input" id="anitracker-${id}-switch"> | |
<label class="custom-control-label" for="anitracker-${id}-switch">${name}</label> | |
</div>`).appendTo(parent); | |
const switc = $(`#anitracker-${id}-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[optionId]) { | |
storage[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',''); | |
} |
It say zip error
Are you following the instructions I put at the top of the file (beneath the "UserScript" stuff)? You don't have to download it.
sweet this is perfect i love this
Hi is there any way to get this for mobile
thanks bro you saved us.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Bro it doesn't work u can't download it and use it ;-;