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