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