Last active
May 19, 2024 20:52
-
-
Save Ellivers/f7716b6b6895802058c367963f3a2c51 to your computer and use it in GitHub Desktop.
AnimePahe Improvements
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name AnimePahe Improvements | |
// @namespace https://gist.github.com/Ellivers/f7716b6b6895802058c367963f3a2c51 | |
// @downloadURL https://gist.github.com/Ellivers/f7716b6b6895802058c367963f3a2c51/raw/anime-tracker.user.js | |
// @match https://animepahe.com/* | |
// @match https://animepahe.org/* | |
// @match https://animepahe.ru/* | |
// @match https://kwik.*/e/* | |
// @match https://kwik.*/f/* | |
// @grant GM_getValue | |
// @grant GM_setValue | |
// @version 3.14 | |
// @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. | |
* 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' | |
* Speed up or slow down a video by holding Ctrl and: | |
* Scrolling up/down | |
* Pressing the up/down keys | |
* You can also hold shift to make the speed change more gradual. | |
* 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. | |
* Focuses on the video player when loading the page, so you don't have to click on it to use keyboard controls. | |
* Automatically chooses the highest quality available when loading the video. | |
* Shows the dates of when episodes were added. | |
* And more! | |
*/ | |
const baseUrl = window.location.toString(); | |
const initialStorage = getStorage(); | |
function getDefaultData() { | |
return {linkList:[], videoTimes:[], bookmarks:[], autoDelete:true, hideThumbnails:false, theatreMode:false, bestQuality:true}; | |
} | |
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 currently has | |
// (You're not actually blocked) | |
if (!kwikDLPageRegex.test(baseUrl) && h1.textContent == "Sorry, you have been blocked") { | |
h1.textContent = "Oops, page failed to load."; | |
document.querySelector('h2').textContent = "Try playing from another page instead."; | |
} | |
return; | |
} | |
scriptElem.onload(() => {anitrackerKwikLoad(window.location.origin + window.location.pathname)}); | |
} | |
function anitrackerKwikLoad(url) { | |
if (kwikDLPageRegex.test(url)) { | |
$(` | |
<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;">[Anime Tracker] 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; | |
} | |
$(` | |
<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(); | |
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; | |
saveData(storage); | |
} | |
if (initialStorage.videoTimes === undefined) { | |
initialStorage.videoTimes = []; | |
saveData(initialStorage); | |
} | |
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); | |
} | |
} | |
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(`[Anime Tracker]\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', updateTime); | |
player.addEventListener('seeked', () => { | |
updateTime(); | |
removeLoadingIndicators(); | |
}); | |
const frametime = 1 / 24; | |
$(document).on('keydown', function(e) { | |
if (e.key === 'ArrowUp') changeSpeed(e, -1); | |
if (e.key === 'ArrowDown') changeSpeed(e, 1); | |
if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) return; | |
if (e.key === 'j') { | |
player.currentTime = Math.max(0, player.currentTime - 10); | |
} | |
else if (e.key === 'l') { | |
player.currentTime = Math.min(player.duration, player.currentTime + 10); | |
} | |
else if (/^Numpad\d$/.test(e.code)) { | |
player.currentTime = (player.duration/10)*(+e.code.replace('Numpad', '')); | |
} | |
if (player.currentTime > 0 && !player.paused && !player.ended && player.readyState > 2) return; | |
if (e.key === ',') { | |
player.currentTime = Math.max(0, player.currentTime - frametime); | |
} | |
else if (e.key === '.') { | |
player.currentTime = Math.min(player.duration, player.currentTime + frametime); | |
} | |
}); | |
// Ctrl+scrolling to change speed | |
$(` | |
<div class="anitracker-video-speed" 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 }); | |
} | |
}; | |
let showSpeedChange = undefined; | |
let settingsContainerId = undefined; | |
for (const elem of $('.plyr__menu__container')) { | |
regex = /plyr\-settings\-(\d+)/.exec(elem.id) | |
if (regex === null) continue; | |
settingsContainerId = regex[1]; | |
} | |
const defaultSpeeds = Array.from($(`#plyr-settings-${settingsContainerId}-speed>div>button`)).map(a => +$(a).attr('value')); | |
function changeSpeed(e, delta) { | |
if (!e.ctrlKey) return; | |
if (delta == 0) return; | |
const speedChange = e.shiftKey ? 0.05 : 0.1; | |
player.playbackRate += speedChange * (delta > 0 ? -1 : 1); | |
player.playbackRate = Math.round(player.playbackRate * 100) / 100; | |
$('.anitracker-video-speed span').text(player.playbackRate + "x"); | |
$('.anitracker-video-speed').css('display', 'flex'); | |
clearTimeout(showSpeedChange); | |
showSpeedChange = setTimeout(() => { | |
$('.anitracker-video-speed').hide(); | |
}, 1000); | |
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"; | |
} | |
} | |
e.preventDefault(); | |
} | |
$(`#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) { | |
console.log('[Anime Tracker]', initialStorage); | |
if (initialStorage.hideThumbnails === true) { | |
hideThumbnails(); | |
} | |
function windowOpen(url, target = '_blank') { | |
$(`<a href="${url}" target="${target}"></a>`)[0].click(); | |
} | |
// -------- Anime Tracker 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: end; | |
flex-wrap: wrap; | |
} | |
#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 { | |
width: 24px; | |
height: 24px; | |
margin: 20px 20px; | |
cursor: pointer; | |
} | |
#anitracker-modal-body { | |
padding: 10px; | |
overflow-x: hidden; | |
} | |
#anitracker-modal-body .anitracker-switch {margin-bottom: 2px;\n} | |
.anitracker-collection-item {list-style: none;\n} | |
.anitracker-collection-item a { | |
font-size: 0.875rem; | |
display: block; | |
padding: 5px 15px; | |
color: rgb(238, 238, 238); | |
text-decoration: none; | |
} | |
.anitracker-collection-item img { | |
margin: auto 0px; | |
width: 50px; | |
height: 50px; | |
border-radius: 100%; | |
} | |
.anitracker-collection-item .anitracker-main-text { | |
font-weight: 700; | |
color: rgb(238, 238, 238); | |
} | |
.anitracker-collection-item .anitracker-subtext { | |
font-size: 0.75rem; | |
color: rgb(153, 153, 153); | |
} | |
.anitracker-collection-item:hover .anitracker-subtext { | |
color: rgb(238, 238, 238); | |
} | |
.anitracker-collection-item:hover { | |
background-color: var(--pink); | |
} | |
.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 {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 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-storage-filter button { | |
height: 42px; | |
border-radius: 7px!important; | |
font-size: 2em; | |
color: #ddd!important; | |
margin-left: 10px!important; | |
} | |
.anitracker-storage-filter button::after { | |
vertical-align: 20px; | |
} | |
.anitracker-storage-filter 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-bookmark-check { | |
color: white; | |
margin-left: -.7rem; | |
font-size: 1rem; | |
vertical-align: super; | |
text-shadow: none; | |
} | |
.anitracker-header-bookmark { | |
margin-right: 1%; | |
margin-left: 1%; | |
color: white; | |
background: none; | |
border: 2px solid white; | |
border-radius: 5px; | |
width: 2rem; | |
} | |
.anitracker-header-bookmark:hover { | |
border-color: #ff006c; | |
color: #ff006c; | |
} | |
@keyframes anitracker-modalOpen { | |
0% { | |
transform: scale(0.5); | |
} | |
20% { | |
transform: scale(1.07); | |
} | |
100% { | |
transform: scale(1); | |
} | |
} | |
@keyframes anitracker-fadeIn { | |
0% { | |
opacity: 0; | |
} | |
100% { | |
opacity: 1; | |
} | |
} | |
`.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: () => { | |
if (window.innerWidth <= 1375) return; | |
$('.theatre>').css('max-width', '80%'); | |
}, | |
offEvent: () => { | |
$('.theatre>').css('max-width', ''); | |
} | |
}, | |
{ | |
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 | |
}]; | |
$(document).on('visibilitychange', () => { | |
if (document.hidden) return; | |
updateSwitches(); | |
}); | |
function playAnimation(elem, anim, type = '', duration) { | |
return new Promise(resolve => { | |
elem.css('animation', `anitracker-${anim} ${duration || animationTimes[anim]}s forwards ${type}`); | |
setTimeout(() => { | |
elem.css('animation', ''); | |
resolve(); | |
}, animationTimes[anim] * 1000); | |
}); | |
} | |
// Anime Tracker modal | |
function addModal() { | |
$(` | |
<div id="anitracker-modal"> | |
<div id="anitracker-modal-content"> | |
<svg id="anitracker-modal-close" fill="#ffffff" height="800px" width="800px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 460.775 460.775" xml:space="preserve"> | |
<path d="M285.08,230.397L456.218,59.27c6.076-6.077,6.076-15.911,0-21.986L423.511,4.565c-2.913-2.911-6.866-4.55-10.992-4.55 c-4.127,0-8.08,1.639-10.993,4.55l-171.138,171.14L59.25,4.565c-2.913-2.911-6.866-4.55-10.993-4.55 c-4.126,0-8.08,1.639-10.992,4.55L4.558,37.284c-6.077,6.075-6.077,15.909,0,21.986l171.138,171.128L4.575,401.505 | |
c-6.074,6.077-6.074,15.911,0,21.986l32.709,32.719c2.911,2.911,6.865,4.55,10.992,4.55c4.127,0,8.08-1.639,10.994-4.55 l171.117-171.12l171.118,171.12c2.913,2.911,6.866,4.55,10.993,4.55c4.128,0,8.081-1.639,10.992-4.55l32.709-32.719 c6.074-6.075,6.074-15.909,0-21.986L285.08,230.397z"></path> | |
</svg> | |
<div id="anitracker-modal-body"></div> | |
</div> | |
</div>`).insertBefore('.main-header'); | |
$('#anitracker-modal').on('click', (e) => { | |
if (e.target !== e.currentTarget) return; | |
closeModal(); | |
}); | |
$('#anitracker-modal-close').on('click', () => { | |
closeModal(); | |
}); | |
$(document).on('keydown', (e) => { | |
if (modalIsOpen() && e.key === 'Escape') { | |
closeModal(); | |
} | |
}); | |
} | |
addModal(); | |
function openModal() { | |
playAnimation($('#anitracker-modal-content'), 'modalOpen'); | |
playAnimation($('#anitracker-modal'), 'fadeIn'); | |
$('#anitracker-modal').css('display','flex'); | |
} | |
function closeModal() { | |
playAnimation($('#anitracker-modal'), 'fadeIn', 'reverse', 0.1).then(() => { | |
$('#anitracker-modal').hide(); | |
}); | |
} | |
function modalIsOpen() { | |
return $('#anitracker-modal').is(':visible'); | |
} | |
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="${baseName} - Collection"> | |
<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'); | |
$('.anitracker-collection').on('click', function() { | |
$('#anitracker-modal-body').empty(); | |
for (const anime of seriesList) { | |
$(` | |
<div class="anitracker-collection-item"> | |
<a href="/anime/${anime.session}" title="${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'); | |
} | |
openModal(); | |
}); | |
} | |
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 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"} | |
], | |
"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; | |
} | |
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 + '?from=customlink'; | |
} | |
if (parts.animeSession !== '' && parts.episodeSession !== '' && parts.time === -1) { | |
return '/play/' + parts.animeSession + '/' + parts.episodeSession + '?from=customlink'; | |
} | |
if (parts.animeSession !== '' && parts.episodeSession !== '' && parts.time >= 0) { | |
return '/play/' + parts.animeSession + '/' + parts.episodeSession + '?time=' + parts.time + '&from=customlink'; | |
} | |
return undefined; | |
})(); | |
if (destination !== undefined) { | |
$('h1').text('Redirecting...'); | |
window.location.replace(destination); | |
} | |
return; | |
} | |
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 ($($('h1')[0]).text().includes('404')) 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; | |
} | |
// Bookmark header button | |
$(` | |
<button class="anitracker-header-bookmark" title="View bookmarks"><i class="fa fa-bookmark"></i></button> | |
`).insertAfter('.navbar-nav'); | |
$('.anitracker-header-bookmark').on('click', () => { | |
$('#anitracker-modal-body').empty(); | |
const storage = getStorage(); | |
$("<h4>Bookmarks</h4>").appendTo('#anitracker-modal-body'); | |
$(` | |
<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.bookmarks.forEach(g => { | |
$(` | |
<div class="anitracker-modal-list-entry" animeid="${g.id}"> | |
<a href="/a/${g.id}"> | |
${g.name} | |
</a><br> | |
<button class="btn btn-danger"> | |
<i class="fa fa-trash" aria-hidden="true"></i> | |
Delete | |
</button> | |
</div>`).appendTo('#anitracker-modal-body .anitracker-modal-list')}); | |
if (storage.bookmarks.length === 0) { | |
$("<span>No bookmarks yet!</span>").appendTo('#anitracker-modal-body .anitracker-modal-list'); | |
} | |
$('.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-check').hide(); | |
} | |
$(e.currentTarget).parent().remove(); | |
}); | |
openModal(); | |
}); | |
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; | |
} | |
// Search/index page | |
if (/^\/anime\/?$/.test(window.location.pathname)) { | |
$(` | |
<div id="anitracker" style="margin-bottom: 10px;"> | |
<button class="btn btn-dark" id="anitracker-random-anime"> | |
<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">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">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">All</button> | |
</div> | |
<div class="btn-group"> | |
<button class="btn btn-dark" id="anitracker-time-search-button"> | |
<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"> | |
<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')) 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'); | |
if (newRule === 'and' && appliedFilters.filter(a => a.startsWith(type + '/')).length > 1 && appliedFilters.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; | |
} | |
} | |
filterBox.focus(); | |
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/fuse.js@6.6.2', 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" 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" 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" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-type-list'); | |
getDropdownButtons(filterValues.type, 'type').forEach(g => { g.appendTo('#anitracker-type-dropdown') }); | |
$(`<div id="anitracker-demographic-dropdown" 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" 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:12px;"> | |
<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('[Anime Tracker]\n\nYear values must both be 4 numbers.'); | |
return; | |
} | |
if (to.year < from.year || (to.year === from.year && to.season < from.season)) { | |
alert('[Anime Tracker]\n\nSeason times must be from oldest to newest.'); | |
return; | |
} | |
if (to.year - from.year > 100) { | |
alert('[Anime Tracker]\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('[Anime Tracker] Search 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/fuse.js@6.6.2', 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) { | |
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 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()) { | |
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; | |
} | |
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('[Anime Tracker]\n\nThe session was outdated, and has been refreshed. Please try that link again.') | |
} | |
window.history.replaceState({}, document.title, window.location.origin + window.location.pathname); | |
} | |
// 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'); | |
addManageDataButton(); | |
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) { | |
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 2; | |
} | |
const obj = getStoredLinkData(initialStorage); | |
const is404 = $('h1').text().includes('404'); | |
if (isEpisode() && !is404) $('#downloadMenu').changeElementType('button'); | |
console.log('[Anime Tracker]', 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) { | |
$('.text-center h1').text('Refreshing session, please wait...'); | |
refreshSession(true); | |
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 found in tracker.'); | |
return; | |
} | |
function getSubInfo(str) { | |
const match = /^\b([^·]+)·\s*(\d{2,4})p(.*)$/.exec(str); | |
return { | |
name: match[1], | |
quality: +match[2], | |
other: match[3] | |
} | |
} | |
// Set the quality to best automatically | |
function bestVideoQuality() { | |
if (!isEpisode()) return; | |
const currentSub = getStoredLinkData(getStorage()).subInfo || getSubInfo($('#resolutionMenu .active').text()); | |
let index = -1; | |
for (let i = 0; i < $('#resolutionMenu').children().length; i++) { | |
const sub = $('#resolutionMenu').children()[i]; | |
const subInfo = getSubInfo($(sub).text()); | |
if (subInfo.name !== currentSub.name || subInfo.other !== currentSub.other) continue; | |
if (subInfo.quality >= currentSub.quality) index = i; | |
} | |
if (index === -1) { | |
return; | |
} | |
const newSub = $('#resolutionMenu').children()[index]; | |
if (!["","Loading..."].includes($('#fansubMenu').text())) { | |
if ($(newSub).text() === $('#resolutionMenu .active').text()) return; | |
newSub.click(); | |
return; | |
} | |
new MutationObserver(function(mutationList, observer) { | |
newSub.click(); | |
observer.disconnect(); | |
}).observe($('#fansubMenu')[0], { childList: true }); | |
} | |
function setIframeUrl(url) { | |
$('.embed-responsive-item').remove(); | |
$(` | |
<iframe class="embed-responsive-item" scrolling="no" allowfullscreen="" allowtransparency="" src="${url}"></iframe> | |
`).prependTo('.embed-responsive'); | |
$('.embed-responsive-item')[0].contentWindow.focus(); | |
} | |
// Fix the quality dropdown buttons | |
if (isEpisode()) { | |
new MutationObserver(function(mutationList, observer) { | |
$('.click-to-load').remove(); | |
$('#resolutionMenu').off('click'); | |
$('#resolutionMenu').on('click', (el) => { | |
const targ = $(el.target); | |
if (targ.data('src') === undefined) return; | |
setIframeUrl(targ.data('src')); | |
$('#resolutionMenu .active').removeClass('active'); | |
targ.addClass('active'); | |
$('#fansubMenu').html(targ.html()); | |
const storage = getStorage(); | |
const data = getStoredLinkData(storage); | |
data.subInfo = getSubInfo(targ.text()); | |
saveData(storage); | |
$.cookie('res', targ.data('resolution'), { | |
expires: 365, | |
path: '/' | |
}); | |
$.cookie('aud', targ.data('audio'), { | |
expires: 365, | |
path: '/' | |
}); | |
$.cookie('av1', targ.data('av1'), { | |
expires: 365, | |
path: '/' | |
}); | |
}); | |
observer.disconnect(); | |
}).observe($('#fansubMenu')[0], { childList: true }); | |
if (initialStorage.bestQuality === true) { | |
bestVideoQuality(); | |
} | |
else if ($('#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); | |
} | |
} | |
} | |
const linkFromArg = paramArray.find(a => a[0] === 'from'); | |
if (linkFromArg !== undefined) { | |
if (isEpisode() && initialStorage.autoDelete && obj !== undefined) { | |
$(` | |
<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">Dismiss</span> | |
</span>`).prependTo('.content-wrapper'); | |
$('.anitracker-from-share-warning>span').on('click', function(e) { | |
$(e.target).parent().remove(); | |
}); | |
} | |
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"> | |
<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(`[Anime Tracker]\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); | |
}); | |
const animeData = getAnimeData(); | |
if (animeData !== undefined) { | |
const animeSession = animeData.session; | |
const firstEpisodes = getResponseData('/api?m=release&sort=episode_asc&id=' + animeSession); | |
const lastEpisodes = getResponseData('/api?m=release&sort=episode_desc&id=' + animeSession); | |
if (firstEpisodes !== undefined && lastEpisodes !== undefined && firstEpisodes.length > 0) { | |
let episode = getEpisodeNum(); | |
if (episode === firstEpisodes[0].episode) { | |
setPrequelPoster(); | |
} | |
if (episode === lastEpisodes[0].episode) { | |
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="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="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() { | |
$('#anitracker-refresh-session').text('Waiting...'); | |
const result = refreshSession(); | |
if (result === 0) { | |
$('#anitracker-refresh-session').text('Refreshing...'); | |
} | |
else if (result === 1) { | |
$('#anitracker-refresh-session').text('Failed: Link not found in tracker'); | |
setTimeout(() => { | |
$('#anitracker-refresh-session').text('Refresh Session'); | |
}, 2200); | |
} | |
else { | |
$('#anitracker-refresh-session').text('Failed.'); | |
setTimeout(() => { | |
$('#anitracker-refresh-session').text('Refresh Session'); | |
}, 2200); | |
} | |
}); | |
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'); | |
}); | |
} | |
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) { | |
const loc = new URL(url); | |
return loc.origin + loc.pathname; | |
} | |
$(` | |
<button class="btn btn-dark" id="anitracker-clear-from-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); | |
} | |
$('#anitracker-clear-from-tracker').text('Cleared!'); | |
setTimeout(() => { | |
$('#anitracker-clear-from-tracker').text('Clear from Tracker'); | |
}, 1500); | |
}); | |
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"> | |
<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(); | |
$('#anitracker-clear-episodes-from-tracker').text('Cleared!'); | |
setTimeout(() => { | |
$('#anitracker-clear-episodes-from-tracker').text('Clear Episodes from Tracker'); | |
}, 1500); | |
}); | |
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/fuse.js@6.6.2', 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(); | |
$(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]); | |
$(` | |
<span style="margin-left:5%;pointer-events:auto;" title="Upload date">${new Date(episodes[i].created_at).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(); | |
[$(` | |
<i title="Bookmark ${animename}" class="fa fa-bookmark anitracker-title-icon anitracker-bookmark-toggle"> | |
<i style="display: none;" class="fa fa-check anitracker-bookmark-check"> | |
</i> | |
</i>`), | |
$(` | |
<a href="/a/${animeid}" title="Get Link" class="fa fa-link btn anitracker-title-icon" data-toggle="modal" data-target="#modalBookmark"></a> | |
`)].forEach(a => a.appendTo('.title-wrapper>h1')); | |
if (initialStorage.bookmarks !== undefined && initialStorage.bookmarks.find(g => g.id === animeid)) { | |
$('.anitracker-bookmark-check').show(); | |
} | |
$('.anitracker-bookmark-toggle').on('click', (e) => { | |
const check = $(e.currentTarget).find('.anitracker-bookmark-check'); | |
if (toggleBookmark(animeid, animename)) { | |
check.show(); | |
return; | |
} | |
check.hide(); | |
}); | |
} | |
function getRandomAnime(list, args, openType = '_blank') { | |
const random = randint(0, list.length-1); | |
windowOpen(list[random].link + args, openType); | |
} | |
function isRandomAnime() { | |
return new URLSearchParams(window.location.search).has('anitracker-random'); | |
} | |
const badCovers = ['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']; | |
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) { | |
setAnimeCover(request.response); | |
} | |
else { | |
new MutationObserver(function(mutationList, observer) { | |
if ($('.anime-cover').css('background-image').length <= 10) return; | |
setAnimeCover(request.response); | |
observer.disconnect(); | |
}).observe($('.anime-cover')[0], { attributes: true }); | |
} | |
}; | |
request.send(); | |
} | |
function trimHttp(string) { | |
return string.replace(/^https?:\/\//,''); | |
} | |
function setAnimeCover(response) { | |
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) 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}")`); | |
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 mergeData(newData, ignoredKeys = []) { | |
const storage = getStorage(); | |
const changed = { | |
linkListAdded: 0, | |
videoTimesAdded: 0, | |
videoTimesUpdated: 0, | |
bookmarksAdded: 0, | |
settingsUpdated: 0 | |
} | |
for (const [key, value] of Object.entries(newData)) { | |
if (getDefaultData()[key] === undefined || ignoredKeys.includes(key)) continue; | |
if (key === 'linkList') { | |
value.forEach(g => { | |
if ((g.type === 'episode' && storage.linkList.find(h => h.type === 'episode' && h.animeSession === g.animeSession && h.episodeSession === g.episodeSession) === undefined) | |
|| (g.type === 'anime' && storage.linkList.find(h => h.type === 'anime' && h.animeSession === g.animeSession) === undefined)) { | |
storage.linkList.push(g); | |
changed.linkListAdded++; | |
} | |
}); | |
continue; | |
} | |
if (key === 'videoTimes') { | |
value.forEach(g => { | |
const foundTime = storage.videoTimes.find(h => h.videoUrls.includes(g.videoUrls[0])); | |
if (foundTime === undefined) { | |
storage.videoTimes.push(g); | |
changed.videoTimesAdded++; | |
} | |
else if (foundTime.time < g.time) { | |
foundTime.time = g.time; | |
changed.videoTimesUpdated++; | |
} | |
}); | |
continue; | |
} | |
if (key === 'bookmarks') { | |
value.forEach(g => { | |
if (storage.bookmarks.find(h => h.id === g.id) !== undefined) return; | |
storage.bookmarks.push(g); | |
changed.bookmarksAdded++; | |
}); | |
continue; | |
} | |
if ((value !== true && value !== false) || storage[key] === undefined || storage[key] === value) continue; | |
changed.settingsUpdated++; | |
storage[key] = value; | |
} | |
saveData(storage); | |
if (changed.settingsUpdated > 0) updateSwitches(); | |
let totalChanged = 0; | |
for (const [key, value] of Object.entries(changed)) { | |
totalChanged += value; | |
} | |
changed.total = totalChanged; | |
return changed; | |
} | |
function addManageDataButton() { | |
$(` | |
<button class="btn btn-dark" id="anitracker-show-data"> | |
<i class="fa fa-floppy-o" aria-hidden="true"></i> | |
Manage Data... | |
</button> | |
<button class="btn btn-dark" id="anitracker-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'); | |
openModal(); | |
}); | |
$('#anitracker-show-data').on('click', function() { | |
$('#anitracker-modal-body').empty(); | |
$(` | |
<div class="anitracker-modal-list-container"> | |
<div class="anitracker-storage-data" 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" 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"> | |
<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"> | |
<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"> | |
<i class="fa fa-download" aria-hidden="true"></i> | |
Export Data | |
</button> | |
</div> | |
<label class="btn btn-secondary" for="anitracker-import-data" style="margin-bottom:0;"> | |
<i class="fa fa-upload" aria-hidden="true"></i> | |
Import Data | |
</label> | |
<input type="file" id="anitracker-import-data" style="visibility: hidden;" 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"> | |
<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"> | |
<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', expandData); | |
$('#anitracker-reset-data').on('click', function() { | |
if (confirm('[Anime Tracker]\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-export-data').on('click', function() { | |
const storage = getStorage(); | |
if (storage.cache) { | |
delete storage.cache; | |
saveData(storage); | |
} | |
download('anime-tracker-data-' + Date.now() + '.json', JSON.stringify(getStorage(), null, 2)); | |
}); | |
$('#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('[Anime Tracker]\n\nPlease input a valid JSON file.'); | |
return; | |
} | |
const changed = mergeData(newData); | |
let finishString = ''; | |
if (changed.total > 0) { | |
finishString = 'Data imported!\n'; | |
if (changed.linkListAdded > 0) finishString += `\nSession entries added: ${changed.linkListAdded}`; | |
if (changed.videoTimesAdded > 0) finishString += `\nVideo progress entries added: ${changed.videoTimesAdded}`; | |
if (changed.videoTimesUpdated > 0) finishString += `\nVideo progress times updated: ${changed.videoTimesUpdated}`; | |
if (changed.bookmarksAdded > 0) finishString += `\nBookmarks added: ${changed.bookmarksAdded}`; | |
if (changed.settingsUpdated > 0) finishString += `\nSettings updated: ${changed.settingsUpdated}`; | |
} | |
else finishString = 'No data was updated.'; | |
alert('[Anime Tracker]\n\n' + finishString); | |
if (changed.total > 0) closeModal(); | |
}); | |
fileReader.readAsText(file); | |
}); | |
function expandData() { | |
const storage = getStorage(); | |
$(this).find('.anitracker-expand-data-icon').replaceWith(contractIcon); | |
const dataEntries = $(this).parent().find('.anitracker-modal-list'); | |
$(` | |
<div class="btn-group anitracker-storage-filter"> | |
<input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-storage-data-search" placeholder="Search"> | |
<button dir="down" class="btn btn-secondary dropdown-toggle"></button> | |
</div> | |
`).appendTo(dataEntries); | |
$(this).parent().find('.anitracker-storage-data-search').focus(); | |
$(this).parent().find('.anitracker-storage-data-search').on('input', (e) => { | |
setTimeout(() => { | |
const query = $(e.target).val(); | |
for (const entry of $(this).parent().find('.anitracker-modal-list-entry')) { | |
if ($($(entry).find('a,span')[0]).text().toLowerCase().includes(query)) { | |
$(entry).show(); | |
continue; | |
} | |
$(entry).hide(); | |
} | |
}, 10); | |
}); | |
$(this).parent().find('.anitracker-storage-filter 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 $(this).parent().find('.anitracker-modal-list-entry')) { | |
entries.push(entry.outerHTML); | |
} | |
entries.reverse(); | |
$(this).parent().find('.anitracker-modal-list-entry').remove(); | |
for (const entry of entries) { | |
$(entry).appendTo($(this).parent().find('.anitracker-modal-list')); | |
} | |
applyDeleteEvents(); | |
}); | |
function applyDeleteEvents() { | |
$('.anitracker-modal-list-entry 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(); | |
}); | |
} | |
if ($(this).attr('key') === 'linkList') { | |
storage.linkList.forEach(g => { | |
$(` | |
<div class="anitracker-modal-list-entry"> | |
<a target="_blank" href="/${(g.type === 'episode' ? 'play/' : 'anime/') + g.animeSession + (g.type === 'episode' ? ('/' + g.episodeSession) : '')}"> | |
${g.animeName}${g.type === 'episode' ? (' - Episode ' + g.episodeNum) : ''} | |
</a><br> | |
<button class="btn btn-danger"> | |
<i class="fa fa-trash" aria-hidden="true"></i> | |
Delete | |
</button> | |
</div>`).appendTo($(this).parent().find('.anitracker-modal-list')); | |
}); | |
applyDeleteEvents(); | |
} | |
else if ($(this).attr('key') === 'videoTimes') { | |
storage.videoTimes.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" lookForUrl="${g.videoUrls[0]}"> | |
<i class="fa fa-trash" aria-hidden="true"></i> | |
Delete | |
</button> | |
</div>`).appendTo($(this).parent().find('.anitracker-modal-list')); | |
}); | |
$('.anitracker-modal-list-entry button').on('click', function() { | |
const storage = getStorage(); | |
storage.videoTimes = storage.videoTimes.filter(g => !g.videoUrls.includes($(this).attr('lookForUrl'))) | |
saveData(storage); | |
$(this).parent().remove(); | |
}); | |
} | |
$(this).off('click', expandData); | |
$(this).on('click', contractData); | |
} | |
function contractData() { | |
$(this).find('.anitracker-expand-data-icon').replaceWith(expandIcon); | |
$(this).parent().find('.anitracker-modal-list').empty(); | |
$(this).off('click', contractData); | |
$(this).on('click', expandData); | |
} | |
openModal(); | |
}); | |
} | |
addManageDataButton(); | |
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"> | |
<button class="btn btn-dark anitracker-copy-button" copy="link-time" data-placement="top" data-content="Copied!">Link & Time</button> | |
</div>`).appendTo('#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') { | |
const videoUrl = stripUrl($('.embed-responsive-item').attr('src')); | |
const time = (() => { | |
for (const time of getStorage().videoTimes) { | |
if (!time.videoUrls.includes(videoUrl)) continue; | |
return time.time; | |
} | |
})(); | |
navigator.clipboard.writeText(window.location.origin + '/customlink?a=' + name + '&e=' + episode + '&t=' + Math.floor(time).toString()); | |
} | |
targ.popover('show'); | |
setTimeout(() => { | |
targ.popover('hide'); | |
}, 1000); | |
}); | |
} | |
if (initialStorage.autoDelete === true && isEpisode() && linkFromArg == 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(); | |
} | |
} | |
if (modalIsOpen()) { | |
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) { | |
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('#anitracker-modal-body'); | |
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',''); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment