-
-
Save Ellivers/f7716b6b6895802058c367963f3a2c51 to your computer and use it in GitHub Desktop.
// ==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',''); | |
} |
real?
Yes, very real. I've spent more than 2 years working on it now.
Thats a really impressive script!! I wish I'd found it earlier.
I recommend publishing it on greasyfork since it's the most popular platform for sharing userscripts and also to help people better find your amazing work. (personally, i just wish I'd have found it much earlier if it were on greasyfork lol)
btw, I'd also like to suggest saving/restoring the last selected player speed.
I think it would be best to keep it simple like -
player.playbackRate = localStorage.getItem("playerSpeed") || 1;
player.onratechange = () => localStorage.setItem("playerSpeed", player.playbackRate);
Bro it doesn't work u can't download it and use it ;-;
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.
real?