AnimePahe Improvements

Improvements and additions for the AnimePahe site

目前为 2024-12-30 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name AnimePahe Improvements
  3. // @namespace https://gist.github.com/Ellivers/f7716b6b6895802058c367963f3a2c51
  4. // @match https://animepahe.com/*
  5. // @match https://animepahe.org/*
  6. // @match https://animepahe.ru/*
  7. // @match https://kwik.*/e/*
  8. // @match https://kwik.*/f/*
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @version 3.23.1
  12. // @author Ellivers
  13. // @license MIT
  14. // @description Improvements and additions for the AnimePahe site
  15. // ==/UserScript==
  16.  
  17. /*
  18. How to install:
  19. * Get the Violentmonkey browser extension (Tampermonkey is largely untested, but seems to work as well).
  20. * For the GitHub Gist page, click the "Raw" button on this page.
  21. * For Greasy Fork, click "Install this script".
  22. * I highly suggest using an ad blocker (uBlock Origin is recommended)
  23.  
  24. Feature list:
  25.  
  26. * 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!
  27. * Saves your watch progress of each video, so you can resume right where you left off.
  28. * The saved data for old sessions can be cleared and is fully viewable and editable.
  29. * Bookmark anime and view it in a bookmark menu.
  30. * Add ongoing anime to an episode feed to easily check when new episodes are out.
  31. * Quickly visit the download page for a video, instead of having to wait 5 seconds when clicking the download link.
  32. * Find collections of anime series in the search results, with the series listed in release order.
  33. * Jump directly to the next anime's first episode from the previous anime's last episode, and the other way around.
  34. * Hide all episode thumbnails on the site, for those who are extra wary of spoilers (and for other reasons).
  35. * Reworked anime index page. You can now:
  36. * Find anime with your desired genre, theme, type, demographic, status and season.
  37. * Search among these filter results.
  38. * Open a random anime within the specified filters and search query.
  39. * Automatically finds a relevant cover for the top of anime pages.
  40. * Frame-by-frame controls on videos, using ',' and '.'
  41. * Skip 10 seconds on videos at a time, using 'j' and 'l'
  42. * Changes the video 'loop' keybind to Shift + L
  43. * Press Shift + N to go to the next episode, and Shift + P to go to the previous one.
  44. * Speed up or slow down a video by holding Ctrl and:
  45. * Scrolling up/down
  46. * Pressing the up/down keys
  47. * You can also hold shift to make the speed change more gradual.
  48. * Enables you to see images from the video while hovering over the progress bar.
  49. * Allows you to also use numpad number keys to seek through videos.
  50. * Theatre mode for a better non-fullscreen video experience on larger screens.
  51. * Instantly loads the video instead of having to click a button to load it.
  52. * 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).
  53. * Adds an "Auto-Play Next" option to automatically go to the next episode when the current one is finished.
  54. * Focuses on the video player when loading the page, so you don't have to click on it to use keyboard controls.
  55. * Adds an option to automatically choose the highest quality available when loading the video.
  56. * Adds a button (in the settings menu) to reset the video player.
  57. * Shows the dates of when episodes were added.
  58. * And more!
  59. */
  60.  
  61. const baseUrl = window.location.toString();
  62. const initialStorage = getStorage();
  63.  
  64. function getDefaultData() {
  65. return {
  66. version: 1,
  67. linkList:[],
  68. videoTimes:[],
  69. bookmarks:[],
  70. notifications: {
  71. lastUpdated: Date.now(),
  72. anime: [],
  73. episodes: []
  74. },
  75. badCovers: [],
  76. autoDelete:true,
  77. hideThumbnails:false,
  78. theatreMode:false,
  79. bestQuality:true,
  80. autoDownload:true,
  81. autoPlayNext:false,
  82. autoPlayVideo:false
  83. };
  84. }
  85.  
  86. function upgradeData(data, fromver) {
  87. console.log(`[AnimePahe Improvements] Upgrading data from version ${fromver === undefined ? 0 : fromver}`);
  88. /* Changes:
  89. * V1:
  90. * autoPlay -> autoPlayNext
  91. */
  92. switch (fromver) {
  93. case undefined:
  94. data.autoPlayNext = data.autoPlay;
  95. delete data.autoPlay;
  96. break;
  97. }
  98. }
  99.  
  100. function getStorage() {
  101. const defa = getDefaultData();
  102. const res = GM_getValue('anime-link-tracker', defa);
  103.  
  104. const oldVersion = res.version;
  105.  
  106. for (const key of Object.keys(defa)) {
  107. if (res[key] !== undefined) continue;
  108. res[key] = defa[key];
  109. }
  110.  
  111. if (oldVersion !== defa.version) {
  112. upgradeData(res, oldVersion);
  113. saveData(res);
  114. }
  115.  
  116. return res;
  117. }
  118.  
  119. function saveData(data) {
  120. GM_setValue('anime-link-tracker', data);
  121. }
  122.  
  123. function secondsToHMS(secs) {
  124. const mins = Math.floor(secs/60);
  125. const hrs = Math.floor(mins/60);
  126. const newSecs = Math.floor(secs % 60);
  127. return `${hrs > 0 ? hrs + ':' : ''}${mins % 60}:${newSecs.toString().length > 1 ? '' : '0'}${newSecs % 60}`;
  128. }
  129.  
  130. function getStoredTime(name, ep, storage, id = undefined) {
  131. if (id !== undefined) {
  132. return storage.videoTimes.find(a => a.episodeNum === ep && a.animeId === id);
  133. }
  134. else return storage.videoTimes.find(a => a.animeName === name && a.episodeNum === ep);
  135. }
  136.  
  137. const kwikDLPageRegex = /^https:\/\/kwik\.\w+\/f\//;
  138.  
  139. // Video player improvements
  140. if (/^https:\/\/kwik\.\w+/.test(baseUrl)) {
  141. if (typeof $ !== "undefined" && $() !== null) anitrackerKwikLoad(window.location.origin + window.location.pathname);
  142. else {
  143. const scriptElem = document.querySelector('head > link:nth-child(12)');
  144. if (scriptElem == null) {
  145. const h1 = document.querySelector('h1');
  146. // Some bug that the kwik DL page had before
  147. // (You're not actually blocked when this happens)
  148. if (!kwikDLPageRegex.test(baseUrl) && h1.textContent == "Sorry, you have been blocked") {
  149. h1.textContent = "Oops, page failed to load.";
  150. document.querySelector('h2').textContent = "This doesn't mean you're blocked. Try playing from another page instead.";
  151. }
  152. return;
  153. }
  154. scriptElem.onload(() => {anitrackerKwikLoad(window.location.origin + window.location.pathname)});
  155. }
  156.  
  157. function anitrackerKwikLoad(url) {
  158. if (kwikDLPageRegex.test(url)) {
  159. if (initialStorage.autoDownload === false) return;
  160. $(`
  161. <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">
  162. <span style="color:white;font-size:3.5em;font-weight:bold;">[AnimePahe Improvements] Downloading...</span>
  163. </div>`).prependTo(document.body);
  164.  
  165. if ($('form').length > 0) {
  166. $('form').submit();
  167. setTimeout(() => {$('#anitrackerKwikDL').remove()}, 1500);
  168. }
  169. else new MutationObserver(function(mutationList, observer) {
  170. if ($('form').length > 0) {
  171. observer.disconnect();
  172. $('form').submit();
  173. setTimeout(() => {$('#anitrackerKwikDL').remove()}, 1500);
  174. }
  175. }).observe(document.body, { childList: true, subtree: true });
  176.  
  177. return;
  178. }
  179.  
  180. if ($('.anitracker-message').length > 0) {
  181. console.log("[AnimePahe Improvements (Player)] Script was reloaded.");
  182. return;
  183. }
  184.  
  185. $(`
  186. <div class="anitracker-loading plyr__control--overlaid" style="opacity: 1; border-radius: 10%;">
  187. <span>Loading...</span>
  188. </div>`).appendTo('.plyr--video');
  189.  
  190. $('button.plyr__controls__item:nth-child(1)').hide();
  191. $('.plyr__progress__container').hide();
  192.  
  193. const player = $('#kwikPlayer')[0];
  194.  
  195. function getVideoInfo() {
  196. const fileName = document.getElementsByClassName('ss-label')[0].textContent;
  197. const nameParts = fileName.split('_');
  198. let name = '';
  199. for (let i = 0; i < nameParts.length; i++) {
  200. const part = nameParts[i];
  201. if (part.trim() === 'AnimePahe') {
  202. i ++;
  203. continue;
  204. }
  205. if (part === 'Dub' && i >= 1 && [2,3,4,5].includes(nameParts[i-1].length)) break;
  206. if (/\d{2}/.test(part) && i >= 1 && nameParts[i-1] === '-') break;
  207.  
  208. name += nameParts[i-1] + ' ';
  209. }
  210. return {
  211. animeName: name.slice(0, -1),
  212. episodeNum: +/^AnimePahe_.+_-_([\d\.]{2,})/.exec(fileName)[1],
  213. resolution: +/^AnimePahe_.+_-_[\d\.]{2,}(?:_[A-Za-z]+)?_(\d+)p/.exec(fileName)[1]
  214. };
  215. }
  216.  
  217. async function handleTimestamps(title, episode) {
  218. const req = new XMLHttpRequest();
  219. req.open('GET', 'https://raw.githubusercontent.com/c032/anidb-animetitles-archive/refs/heads/main/data/animetitles.json', true);
  220. req.onload = () => {
  221. if (req.status !== 200) return;
  222. const data = req.response.split('\n');
  223.  
  224. let anidbId = undefined;
  225. for (const anime of data) {
  226. const obj = JSON.parse(anime);
  227. if (obj.titles.find(a => a.title === title) === undefined) continue;
  228. anidbId = obj.id;
  229. break;
  230. }
  231.  
  232. if (anidbId === undefined) return;
  233.  
  234. const req2 = new XMLHttpRequest();
  235. req2.open('GET', 'https://raw.githubusercontent.com/jonbarrow/open-anime-timestamps/refs/heads/master/timestamps.json', true); // Timestamp data
  236. req2.onload = () => {
  237. if (req.status !== 200) return;
  238. const data = JSON.parse(req2.response)[anidbId];
  239. if (data === undefined) {
  240. console.log('[AnimePahe Improvements] Could not find timestamp data.');
  241. return;
  242. }
  243. console.log(data);
  244. }
  245. req2.send();
  246. }
  247. req.send();
  248. }
  249.  
  250. function updateTime() {
  251. const currentTime = player.currentTime;
  252. const storage = getStorage();
  253.  
  254. // Delete the storage entry
  255. if (player.duration - currentTime <= 20) {
  256. const videoInfo = getVideoInfo();
  257. storage.videoTimes = storage.videoTimes.filter(a => !(a.animeName === videoInfo.animeName && a.episodeNum === videoInfo.episodeNum));
  258. saveData(storage);
  259. return;
  260. }
  261. if (waitingState.idRequest === 1) return;
  262. const vidInfo = getVideoInfo();
  263. const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage);
  264.  
  265. if (storedVideoTime === undefined) {
  266. if (![-1,0].includes(waitingState.idRequest)) { // If the video has loaded (>0) and getting the ID has not failed (-1)
  267. waitingState.idRequest = 1;
  268. sendMessage({action: "id_request"});
  269. setTimeout(() => {
  270. if (waitingState.idRequest === 1) {
  271. waitingState.idRequest = -1; // Failed to get the anime ID from the main page within 2 seconds
  272. updateTime();
  273. }
  274. }, 2000);
  275. return;
  276. }
  277. const vidInfo = getVideoInfo();
  278. storage.videoTimes.push({
  279. videoUrls: [url],
  280. time: player.currentTime,
  281. animeName: vidInfo.animeName,
  282. episodeNum: vidInfo.episodeNum
  283. });
  284. if (storage.videoTimes.length > 1000) {
  285. storage.splice(0,1);
  286. }
  287. saveData(storage);
  288. return;
  289. }
  290.  
  291. storedVideoTime.time = player.currentTime;
  292. if (storedVideoTime.playbackRate !== undefined || player.playbackRate !== 1) storedVideoTime.playbackRate = player.playbackRate;
  293. saveData(storage);
  294. }
  295.  
  296. if (initialStorage.videoTimes === undefined) {
  297. const storage = getStorage();
  298. storage.videoTimes = [];
  299. saveData(storage);
  300. }
  301.  
  302. // For message requests from the main page
  303. // -1: failed
  304. // 0: hasn't started
  305. // 1: waiting
  306. // 2: succeeded
  307. const waitingState = {
  308. idRequest: 0,
  309. videoUrlRequest: 0
  310. };
  311. // Messages received from main page
  312. window.onmessage = function(e) {
  313. const data = e.data;
  314. const action = data.action;
  315. if (action === 'id_response' && waitingState.idRequest === 1) {
  316. const storage = getStorage();
  317. storage.videoTimes.push({
  318. videoUrls: [url],
  319. time: 0,
  320. animeName: getVideoInfo().animeName,
  321. episodeNum: getVideoInfo().episodeNum,
  322. animeId: data.id
  323. });
  324. if (storage.videoTimes.length > 1000) {
  325. storage.splice(0,1);
  326. }
  327. saveData(storage);
  328. waitingState.idRequest = 2;
  329.  
  330. /* WIP feature
  331. const episodeObj = storage.linkList.find(a => a.type === 'episode' && a.animeId === data.id);
  332. if (episodeObj === undefined) return;
  333. handleTimestamps(episodeObj.animeName, episodeObj.episodeNum);*/
  334.  
  335. return;
  336. }
  337. else if (action === 'video_url_response' && waitingState.videoUrlRequest === 1) {
  338. const request = new XMLHttpRequest();
  339. request.open('GET', data.url, true);
  340. request.onload = () => {
  341. if (request.status !== 200) {
  342. console.error('[AnimePahe Improvements] Could not get kwik page for video source');
  343. return;
  344. }
  345.  
  346. const pageElements = Array.from($(request.response)); // Elements that are not buried cannot be found with jQuery.find()
  347. const hostInfo = (() => {
  348. for (const link of pageElements.filter(a => a.tagName === 'LINK')) {
  349. const href = $(link).attr('href');
  350. if (!href.includes('vault')) continue;
  351. const result = /vault-(\d+)\.(\w+\.\w+)$/.exec(href);
  352. return {
  353. vaultId: result[1],
  354. hostName: result[2]
  355. }
  356. break;
  357. }
  358. })();
  359.  
  360. const searchInfo = (() => {
  361. for (const script of pageElements.filter(a => a.tagName === 'SCRIPT')) {
  362. if ($(script).attr('url') !== undefined || !$(script).text().startsWith('eval')) continue;
  363. const result = /(\w{64})\|((?:\w+\|){4,5})https/.exec($(script).text());
  364. let extraNumber = undefined;
  365. 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)
  366. if (extraNumber === undefined) {
  367. const result2 = /q=\\'\w+:\/{2}\w+\-\w+\.\w+\.\w+\/((?:\w+\/)+)/.exec($(script).text());
  368. result2[1].split('/').forEach(a => {if (/\d{2}/.test(a) && a !== hostInfo.vaultId) extraNumber = a;});
  369. }
  370. return {
  371. part1: extraNumber,
  372. part2: result[1]
  373. };
  374. break;
  375. }
  376. })();
  377.  
  378. if (searchInfo.part1 === undefined) {
  379. console.error('[AnimePahe Improvements] Could not find "extraNumber" from ' + data.url);
  380. return;
  381. }
  382.  
  383. waitingState.videoUrlRequest = 2;
  384.  
  385. setupSeekThumbnails(`https://vault-${hostInfo.vaultId}.${hostInfo.hostName}/stream/${hostInfo.vaultId}/${searchInfo.part1}/${searchInfo.part2}/uwu.m3u8`);
  386. };
  387. request.send();
  388. }
  389. else if (action === 'change_time') {
  390. if (data.time !== undefined) player.currentTime = data.time;
  391. }
  392. else if (action === 'key') {
  393. if ([' ','k'].includes(data.key)) {
  394. if (player.paused) player.play();
  395. else player.pause();
  396. }
  397. else if (data.key === 'ArrowLeft') {
  398. player.currentTime = Math.max(0, player.currentTime - 5);
  399. return;
  400. }
  401. else if (data.key === 'ArrowRight') {
  402. player.currentTime = Math.min(player.duration, player.currentTime + 5);
  403. return;
  404. }
  405. else if (/^\d$/.test(data.key)) {
  406. player.currentTime = (player.duration/10)*(+data.key);
  407. return;
  408. }
  409. else if (data.key === 'm') player.muted = !player.muted;
  410. else $(player).trigger('keydown', {
  411. key: data.key
  412. });
  413. }
  414. };
  415.  
  416. player.addEventListener('loadeddata', function loadVideoData() {
  417. const storage = getStorage();
  418. const vidInfo = getVideoInfo();
  419. const storedVideoTime = getStoredTime(vidInfo.animeName, vidInfo.episodeNum, storage);
  420.  
  421. if (storedVideoTime !== undefined) {
  422. player.currentTime = Math.max(0, Math.min(storedVideoTime.time, player.duration));
  423. if (!storedVideoTime.videoUrls.includes(url)) {
  424. storedVideoTime.videoUrls.push(url);
  425. saveData(storage);
  426. }
  427. if (![undefined,1].includes(storedVideoTime.playbackRate)) {
  428. setSpeed(storedVideoTime.playbackRate);
  429. }
  430. else player.playbackRate = 1;
  431. }
  432. else {
  433. player.playbackRate = 1;
  434. waitingState.idRequest = 1;
  435. sendMessage({action: "id_request"});
  436. setTimeout(() => {
  437. if (waitingState.idRequest === 1) {
  438. waitingState.idRequest = -1; // Failed to get the anime ID from the main page within 2 seconds
  439. updateTime();
  440. }
  441. }, 2000);
  442. removeLoadingIndicators();
  443. }
  444.  
  445. const timeArg = Array.from(new URLSearchParams(window.location.search)).find(a => a[0] === 'time');
  446. if (timeArg !== undefined) {
  447. const newTime = +timeArg[1];
  448. if (storedVideoTime === undefined || (storedVideoTime !== undefined && Math.floor(storedVideoTime.time) === Math.floor(newTime)) || (storedVideoTime !== undefined &&
  449. 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)}?`))) {
  450. player.currentTime = Math.max(0, Math.min(newTime, player.duration));
  451. }
  452. window.history.replaceState({}, document.title, url);
  453. }
  454.  
  455. player.removeEventListener('loadeddata', loadVideoData);
  456.  
  457. // Set up events
  458. let lastTimeUpdate = 0;
  459. player.addEventListener('timeupdate', function() {
  460. if (Math.trunc(player.currentTime) % 10 === 0 && player.currentTime - lastTimeUpdate > 9) {
  461. updateTime();
  462. lastTimeUpdate = player.currentTime;
  463. }
  464. });
  465.  
  466. player.addEventListener('pause', () => {
  467. updateTime();
  468. });
  469.  
  470. player.addEventListener('seeked', () => {
  471. updateTime();
  472. removeLoadingIndicators();
  473. });
  474.  
  475. if (storage.autoPlayVideo === true) {
  476. player.play()
  477. }
  478. });
  479.  
  480. function getFrame(video, time, dimensions) {
  481. return new Promise((resolve) => {
  482. video.onseeked = () => {
  483. const canvas = document.createElement('canvas');
  484. canvas.height = dimensions.y;
  485. canvas.width = dimensions.x;
  486. const ctx = canvas.getContext('2d');
  487. ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  488. resolve(canvas.toDataURL('image/png'));
  489. };
  490. try {
  491. video.currentTime = time;
  492. }
  493. catch (e) {
  494. console.error(time, e);
  495. }
  496. });
  497. }
  498.  
  499. const settingsContainerId = (() => {
  500. for (const elem of $('.plyr__menu__container')) {
  501. const regex = /plyr\-settings\-(\d+)/.exec(elem.id);
  502. if (regex === null) continue;
  503. return regex[1];
  504. }
  505. return undefined;
  506. })();
  507.  
  508. function setupSeekThumbnails(videoSource) {
  509. const resolution = 167;
  510.  
  511. const bgVid = document.createElement('video');
  512. bgVid.height = resolution;
  513. bgVid.onloadeddata = () => {
  514. const fullDuration = bgVid.duration;
  515. const timeBetweenThumbnails = fullDuration/(24*6); // Just something arbitrary that seems good
  516. const thumbnails = [];
  517. const aspectRatio = bgVid.videoWidth / bgVid.videoHeight;
  518.  
  519. const aspectRatioCss = `${bgVid.videoWidth} / ${bgVid.videoHeight}`;
  520. const mainStyles = [
  521. "width: 219px",
  522. "aspect-ratio: " + aspectRatioCss,
  523. "padding: 5px",
  524. "opacity:0",
  525. "position: absolute",
  526. "left:0%",
  527. "bottom: 100%",
  528. "background-color: rgba(255,255,255,0.88)",
  529. "border-radius: 8px",
  530. "transition: translate .2s ease .1s,scale .2s ease .1s,opacity .1s ease .05s",
  531. "transform: translate(-50%,0)",
  532. "user-select: none",
  533. "pointer-events: none"
  534. ]
  535.  
  536. $('.plyr__progress .plyr__tooltip').remove();
  537. $(`
  538. <div class="anitracker-progress-tooltip" style="${mainStyles.join(';')};">
  539. <div class="anitracker-progress-image" style="height: 100%; width: 100%; background-color: gray; display:flex; flex-direction: column; align-items: center; overflow: hidden; border-radius: 5px;">
  540. <img style="display: none; width: 100%; aspect-ratio: ${aspectRatioCss};">
  541. <span style="font-size: .9em; bottom: 5px; position: fixed; background-color: rgba(0,0,0,0.7); border-radius: 3px; padding: 0 4px 0 4px;">0:00</span>
  542. </div>
  543. </div>`).insertAfter(`progress`);
  544.  
  545. $('.anitracker-progress-tooltip img').on('load', () => {
  546. $('.anitracker-progress-tooltip img').css('display', 'block');
  547. });
  548.  
  549. const toggleVisibility = (on) => {
  550. if (on) $('.anitracker-progress-tooltip').css('opacity', '1').css('scale','1').css('translate','');
  551. else $('.anitracker-progress-tooltip').css('opacity', '0').css('scale','0.75').css('translate','-12.5% 20px');
  552. };
  553.  
  554. const elem = $('.anitracker-progress-tooltip');
  555. let currentTime = 0;
  556. new MutationObserver(function(mutationList, observer) {
  557. if ($('.plyr--full-ui').hasClass('plyr--hide-controls') || !$(`#plyr-seek-${settingsContainerId}`)[0].matches(':hover')) {
  558. toggleVisibility(false);
  559. return;
  560. }
  561. toggleVisibility(true);
  562.  
  563. const seekValue = $(`#plyr-seek-${settingsContainerId}`).attr('seek-value');
  564. const time = seekValue !== undefined ? Math.min(Math.max(Math.trunc(fullDuration*(+seekValue/100)), 0), fullDuration) : Math.trunc(player.currentTime);
  565. const roundedTime = Math.trunc(time/timeBetweenThumbnails)*timeBetweenThumbnails;
  566. const timeSlot = Math.trunc(time/timeBetweenThumbnails);
  567.  
  568. elem.find('span').text(secondsToHMS(time));
  569. elem.css('left', seekValue + '%');
  570.  
  571. if (roundedTime === Math.trunc(currentTime/timeBetweenThumbnails)*timeBetweenThumbnails) return;
  572.  
  573. const cached = thumbnails.find(a => a.time === timeSlot);
  574. if (cached !== undefined) {
  575. elem.find('img').attr('src', cached.data);
  576. }
  577. else {
  578. elem.find('img').css('display', 'none');
  579. getFrame(bgVid, roundedTime, {y: resolution, x: resolution*aspectRatio}).then((response) => {
  580. thumbnails.push({
  581. time: timeSlot,
  582. data: response
  583. });
  584.  
  585. elem.find('img').css('display', 'none');
  586. elem.find('img').attr('src', response);
  587. });
  588. }
  589. currentTime = time;
  590.  
  591. }).observe($(`#plyr-seek-${settingsContainerId}`)[0], { attributes: true });
  592.  
  593. $(`#plyr-seek-${settingsContainerId}`).on('mouseleave', () => {
  594. toggleVisibility(false);
  595. });
  596.  
  597. }
  598.  
  599. const hls2 = new Hls({
  600. maxBufferLength: 0.1,
  601. backBufferLength: 0,
  602. capLevelToPlayerSize: true,
  603. maxAudioFramesDrift: Infinity
  604. });
  605. hls2.loadSource(videoSource);
  606. hls2.attachMedia(bgVid);
  607. }
  608.  
  609. // Thumbnails when seeking
  610. if (Hls.isSupported()) {
  611. sendMessage({action:"video_url_request"});
  612. waitingState.videoUrlRequest = 1;
  613. setTimeout(() => {
  614. if (waitingState.videoUrlRequest === 2) return;
  615.  
  616. waitingState.videoUrlRequest = -1;
  617. if (typeof hls !== "undefined") setupSeekThumbnails(hls.url);
  618. }, 500);
  619. }
  620.  
  621. function removeLoadingIndicators() {
  622. $('.anitracker-loading').remove();
  623. $('button.plyr__controls__item:nth-child(1)').show();
  624. $('.plyr__progress__container').show();
  625. }
  626.  
  627. let messageTimeout = undefined;
  628.  
  629. function showMessage(text) {
  630. $('.anitracker-message span').text(text);
  631. $('.anitracker-message').css('display', 'flex');
  632. clearTimeout(messageTimeout);
  633. messageTimeout = setTimeout(() => {
  634. $('.anitracker-message').hide();
  635. }, 1000);
  636. }
  637.  
  638. const frametime = 1 / 24;
  639. let funPitch = "";
  640.  
  641. $(document).on('keydown', function(e, other = undefined) {
  642. const key = e.key || other.key;
  643. if (key === 'ArrowUp') {
  644. changeSpeed(e, -1); // The changeSpeed function only works if ctrl is being held
  645. return;
  646. }
  647. if (key === 'ArrowDown') {
  648. changeSpeed(e, 1);
  649. return;
  650. }
  651. if (e.shiftKey && ['l','L'].includes(key)) {
  652. showMessage('Loop: ' + (player.loop ? 'Off' : 'On'));
  653. player.loop = !player.loop;
  654. return;
  655. }
  656. if (e.shiftKey && ['n','N'].includes(key)) {
  657. sendMessage({action: "next"});
  658. return;
  659. }
  660. if (e.shiftKey && ['p','P'].includes(key)) {
  661. sendMessage({action: "previous"});
  662. return;
  663. }
  664. if (e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) return; // Prevents special keys for the rest of the keybinds
  665. if (key === 'j') {
  666. player.currentTime = Math.max(0, player.currentTime - 10);
  667. return;
  668. }
  669. else if (key === 'l') {
  670. player.currentTime = Math.min(player.duration, player.currentTime + 10);
  671. setTimeout(() => {
  672. player.loop = false;
  673. }, 5);
  674. return;
  675. }
  676. else if (/^Numpad\d$/.test(e.code)) {
  677. player.currentTime = (player.duration/10)*(+e.code.replace('Numpad', ''));
  678. return;
  679. }
  680. if (!(player.currentTime > 0 && !player.paused && !player.ended && player.readyState > 2)) {
  681. if (key === ',') {
  682. player.currentTime = Math.max(0, player.currentTime - frametime);
  683. return;
  684. }
  685. else if (key === '.') {
  686. player.currentTime = Math.min(player.duration, player.currentTime + frametime);
  687. return;
  688. }
  689. }
  690.  
  691. funPitch += key;
  692. if (funPitch === 'crazy') {
  693. player.preservesPitch = !player.preservesPitch;
  694. showMessage(player.preservesPitch ? 'Off' : 'Change speed ;D');
  695. funPitch = "";
  696. return;
  697. }
  698. if (!"crazy".startsWith(funPitch)) {
  699. funPitch = "";
  700. }
  701.  
  702. sendMessage({
  703. action: "key",
  704. key: key
  705. });
  706.  
  707. });
  708.  
  709. // Ctrl+scrolling to change speed
  710.  
  711. $(`
  712. <div class="anitracker-message" style="width:50%;height:10%;position:absolute;background-color:rgba(0,0,0,0.5);display:none;justify-content:center;align-items:center;margin-top:1.5%;border-radius:20px;">
  713. <span style="color: white;font-size: 2.5em;">2.0x</span>
  714. </div>`).appendTo($(player).parents().eq(1));
  715.  
  716. jQuery.event.special.wheel = {
  717. setup: function( _, ns, handle ){
  718. this.addEventListener("wheel", handle, { passive: false });
  719. }
  720. };
  721.  
  722. const defaultSpeeds = player.plyr.options.speed;
  723.  
  724. function changeSpeed(e, delta) {
  725. if (!e.ctrlKey) return;
  726. e.preventDefault();
  727. if (delta == 0) return;
  728.  
  729. const speedChange = e.shiftKey ? 0.05 : 0.1;
  730.  
  731. setSpeed(player.playbackRate + speedChange * (delta > 0 ? -1 : 1));
  732. }
  733.  
  734. function setSpeed(speed) {
  735. if (speed > 0) player.playbackRate = Math.round(speed * 100) / 100;
  736. showMessage(player.playbackRate + "x");
  737.  
  738. if (defaultSpeeds.includes(player.playbackRate)) {
  739. $('.anitracker-custom-speed-btn').remove();
  740. }
  741. else if ($('.anitracker-custom-speed-btn').length === 0) {
  742. $(`#plyr-settings-${settingsContainerId}-speed>div>button`).attr('aria-checked','false');
  743. $(`
  744. <button type="button" role="menuitemradio" class="plyr__control anitracker-custom-speed-btn" aria-checked="true"><span>Custom</span></button>
  745. `).prependTo(`#plyr-settings-${settingsContainerId}-speed>div`);
  746.  
  747. for (const elem of $(`#plyr-settings-${settingsContainerId}-home>div>`)) {
  748. if (!/^Speed/.test($(elem).children('span')[0].textContent)) continue;
  749. $(elem).find('span')[1].textContent = "Custom";
  750. }
  751. }
  752. }
  753.  
  754. $(`#plyr-settings-${settingsContainerId}-speed>div>button`).on('click', (e) => {
  755. $('.anitracker-custom-speed-btn').remove();
  756. });
  757.  
  758. $(document).on('wheel', function(e) {
  759. changeSpeed(e, e.originalEvent.deltaY);
  760. });
  761.  
  762. }
  763.  
  764. return;
  765. }
  766.  
  767. if ($() !== null) anitrackerLoad(window.location.origin + window.location.pathname + window.location.search);
  768. else {
  769. document.querySelector('head > link:nth-child(10)').onload(() => {anitrackerLoad(window.location.origin + window.location.pathname + window.location.search)});
  770. }
  771.  
  772. function anitrackerLoad(url) {
  773.  
  774. if ($('#anitracker-modal').length > 0) {
  775. console.log("[AnimePahe Improvements] Script was reloaded.");
  776. return;
  777. }
  778.  
  779. if (initialStorage.hideThumbnails === true) {
  780. hideThumbnails();
  781. }
  782.  
  783. function windowOpen(url, target = '_blank') {
  784. $(`<a href="${url}" target="${target}"></a>`)[0].click();
  785. }
  786.  
  787. (function($) {
  788. $.fn.changeElementType = function(newType) {
  789. let attrs = {};
  790.  
  791. $.each(this[0].attributes, function(idx, attr) {
  792. attrs[attr.nodeName] = attr.nodeValue;
  793. });
  794.  
  795. this.replaceWith(function() {
  796. return $("<" + newType + "/>", attrs).append($(this).contents());
  797. });
  798. };
  799. $.fn.replaceClass = function(oldClass, newClass) {
  800. this.removeClass(oldClass).addClass(newClass);
  801. };
  802. })(jQuery);
  803.  
  804. // -------- AnimePahe Improvements CSS ---------
  805.  
  806. $("head").append('<style id="anitracker-style" type="text/css"></style>');
  807. const sheet = $("#anitracker-style")[0].sheet;
  808.  
  809. const animationTimes = {
  810. modalOpen: 0.2,
  811. fadeIn: 0.2
  812. };
  813.  
  814. const rules = `
  815. #anitracker {
  816. display: flex;
  817. flex-direction: row;
  818. gap: 15px 7px;
  819. align-items: center;
  820. flex-wrap: wrap;
  821. }
  822. .anitracker-index {
  823. align-items: end !important;
  824. }
  825. #anitracker>span {align-self: center;\n}
  826. #anitracker-modal {
  827. position: fixed;
  828. width: 100%;
  829. height: 100%;
  830. background-color: rgba(0,0,0,0.6);
  831. z-index: 20;
  832. display: none;
  833. }
  834. #anitracker-modal-content {
  835. max-height: 90%;
  836. background-color: var(--dark);
  837. margin: auto auto auto auto;
  838. border-radius: 20px;
  839. display: flex;
  840. padding: 20px;
  841. z-index:50;
  842. }
  843. #anitracker-modal-close {
  844. font-size: 2.5em;
  845. margin: 3px 10px;
  846. cursor: pointer;
  847. height: 1em;
  848. }
  849. #anitracker-modal-close:hover {
  850. color: rgb(255, 0, 108);
  851. }
  852. #anitracker-modal-body {
  853. padding: 10px;
  854. overflow-x: hidden;
  855. }
  856. #anitracker-modal-body .anitracker-switch {margin-bottom: 2px;\n}
  857. .anitracker-big-list-item {
  858. list-style: none;
  859. border-radius: 10px;
  860. margin-top: 5px;
  861. }
  862. .anitracker-big-list-item>a {
  863. font-size: 0.875rem;
  864. display: block;
  865. padding: 5px 15px;
  866. color: rgb(238, 238, 238);
  867. text-decoration: none;
  868. }
  869. .anitracker-big-list-item img {
  870. margin: auto 0px;
  871. width: 50px;
  872. height: 50px;
  873. border-radius: 100%;
  874. }
  875. .anitracker-big-list-item .anitracker-main-text {
  876. font-weight: 700;
  877. color: rgb(238, 238, 238);
  878. }
  879. .anitracker-big-list-item .anitracker-subtext {
  880. font-size: 0.75rem;
  881. color: rgb(153, 153, 153);
  882. }
  883. .anitracker-big-list-item:hover .anitracker-main-text {
  884. color: rgb(238, 238, 238);
  885. }
  886. .anitracker-big-list-item:hover .anitracker-subtext {
  887. color: rgb(238, 238, 238);
  888. }
  889. .anitracker-big-list-item:hover {
  890. background-color: #111;
  891. }
  892. .anitracker-big-list-item:focus-within .anitracker-main-text {
  893. color: rgb(238, 238, 238);
  894. }
  895. .anitracker-big-list-item:focus-within .anitracker-subtext {
  896. color: rgb(238, 238, 238);
  897. }
  898. .anitracker-big-list-item:focus-within {
  899. background-color: #111;
  900. }
  901. .anitracker-hide-thumbnails .anitracker-thumbnail img {display: none;\n}
  902. .anitracker-hide-thumbnails .anitracker-thumbnail {
  903. border: 10px solid rgb(32, 32, 32);
  904. aspect-ratio: 16/9;
  905. }
  906. .anitracker-hide-thumbnails .episode-snapshot img {
  907. display: none;
  908. }
  909. .anitracker-hide-thumbnails .episode-snapshot {
  910. border: 4px solid var(--dark);
  911. }
  912. .anitracker-download-spinner {display: inline;\n}
  913. .anitracker-download-spinner .spinner-border {
  914. height: 0.875rem;
  915. width: 0.875rem;
  916. }
  917. .anitracker-dropdown-content {
  918. display: none;
  919. position: absolute;
  920. min-width: 100px;
  921. box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
  922. z-index: 1;
  923. max-height: 400px;
  924. overflow-y: auto;
  925. overflow-x: hidden;
  926. background-color: #171717;
  927. }
  928. .anitracker-dropdown-content button {
  929. color: white;
  930. padding: 12px 16px;
  931. text-decoration: none;
  932. display: block;
  933. width:100%;
  934. background-color: #171717;
  935. border: none;
  936. margin: 0;
  937. }
  938. .anitracker-dropdown-content button:hover, .anitracker-dropdown-content button:focus {background-color: black;\n}
  939. .anitracker-active, .anitracker-active:hover, .anitracker-active:active {
  940. color: white!important;
  941. background-color: #d5015b!important;
  942. }
  943. .anitracker-dropdown-content a:hover {background-color: #ddd;\n}
  944. .anitracker-dropdown:hover .anitracker-dropdown-content {display: block;\n}
  945. .anitracker-dropdown:hover .anitracker-dropbtn {background-color: #bc0150;\n}
  946. #pickDownload span, #scrollArea span {
  947. cursor: pointer;
  948. font-size: 0.875rem;
  949. }
  950. .anitracker-expand-data-icon {
  951. font-size: 24px;
  952. float: right;
  953. margin-top: 6px;
  954. margin-right: 8px;
  955. }
  956. .anitracker-modal-list-container {
  957. background-color: rgb(40,45,50);
  958. margin-bottom: 10px;
  959. border-radius: 12px;
  960. }
  961. .anitracker-storage-data {
  962. background-color: rgb(40,45,50);
  963. border-radius: 12px;
  964. cursor: pointer;
  965. position: relative;
  966. z-index: 1;
  967. }
  968. .anitracker-storage-data:focus {
  969. box-shadow: 0 0 0 .2rem rgb(255, 255, 255);
  970. }
  971. .anitracker-storage-data span {
  972. display:inline-block;
  973. font-size: 1.4em;
  974. font-weight: bold;
  975. }
  976. .anitracker-storage-data, .anitracker-modal-list {
  977. padding: 10px;
  978. }
  979. .anitracker-modal-list-entry {margin-top: 8px;\n}
  980. .anitracker-modal-list-entry a {text-decoration: underline;\n}
  981. .anitracker-modal-list-entry:hover {background-color: rgb(30,30,30);\n}
  982. .anitracker-modal-list-entry button {
  983. padding-top: 0;
  984. padding-bottom: 0;
  985. }
  986. .anitracker-relation-link {
  987. text-overflow: ellipsis;
  988. overflow: hidden;
  989. }
  990. #anitracker-cover-spinner .spinner-border {
  991. width:2rem;
  992. height:2rem;
  993. }
  994. .anime-cover {
  995. display: flex;
  996. justify-content: center;
  997. align-items: center;
  998. image-rendering: optimizequality;
  999. }
  1000.  
  1001. .anitracker-items-box {
  1002. width: 150px;
  1003. display: inline-block;
  1004. }
  1005. .anitracker-items-box > div {
  1006. height:45px;
  1007. width:100%;
  1008. border-bottom: 2px solid #454d54;
  1009. }
  1010. .anitracker-items-box > button {
  1011. background: none;
  1012. border: 1px solid #ccc;
  1013. color: white;
  1014. padding: 0;
  1015. margin-left: 110px;
  1016. vertical-align: bottom;
  1017. border-radius: 5px;
  1018. line-height: 1em;
  1019. width: 2.5em;
  1020. font-size: .8em;
  1021. padding-bottom: .1em;
  1022. margin-bottom: 2px;
  1023. }
  1024. .anitracker-items-box > button:hover {
  1025. background: #ccc;
  1026. color: black;
  1027. }
  1028. .anitracker-items-box-search {
  1029. position: absolute;
  1030. max-width: 150px;
  1031. max-height: 45px;
  1032. min-width: 150px;
  1033. min-height: 45px;
  1034. overflow-wrap: break-word;
  1035. overflow-y: auto;
  1036. }
  1037. .anitracker-items-box .placeholder {
  1038. color: #999;
  1039. position: absolute;
  1040. z-index: -1;
  1041. }
  1042. .anitracker-filter-icon {
  1043. padding: 2px;
  1044. background-color: #d5015b;
  1045. border-radius: 5px;
  1046. display: inline-block;
  1047. cursor: pointer;
  1048. }
  1049. .anitracker-filter-icon:hover {
  1050. border: 1px solid white;
  1051. }
  1052. .anitracker-text-input {
  1053. display: inline-block;
  1054. height: 1em;
  1055. }
  1056. .anitracker-text-input-bar {
  1057. background: #333;
  1058. box-shadow: none;
  1059. color: #bbb;
  1060. }
  1061. .anitracker-text-input-bar:focus {
  1062. border-color: #d5015b;
  1063. background: none;
  1064. box-shadow: none;
  1065. color: #ddd;
  1066. }
  1067. .anitracker-list-btn {
  1068. height: 42px;
  1069. border-radius: 7px!important;
  1070. color: #ddd!important;
  1071. margin-left: 10px!important;
  1072. }
  1073. .anitracker-reverse-order-button {
  1074. font-size: 2em;
  1075. }
  1076. .anitracker-reverse-order-button::after {
  1077. vertical-align: 20px;
  1078. }
  1079. .anitracker-reverse-order-button.anitracker-up::after {
  1080. border-top: 0;
  1081. border-bottom: .3em solid;
  1082. vertical-align: 22px;
  1083. }
  1084. #anitracker-time-search-button svg {
  1085. width: 24px;
  1086. vertical-align: bottom;
  1087. }
  1088. .anitracker-season-group {
  1089. display: grid;
  1090. grid-template-columns: 10% 30% 20% 10%;
  1091. margin-bottom: 5px;
  1092. }
  1093. .anitracker-season-group .btn-group {
  1094. margin-left: 5px;
  1095. }
  1096. a.youtube-preview::before {
  1097. -webkit-transition: opacity .2s linear!important;
  1098. -moz-transition: opacity .2s linear!important;
  1099. transition: opacity .2s linear!important;
  1100. }
  1101. .anitracker-replaced-cover {background-position-y: 25%;\n}
  1102. .anitracker-text-button {
  1103. color:#d5015b;
  1104. cursor:pointer;
  1105. user-select:none;
  1106. }
  1107. .anitracker-text-button:hover {
  1108. color:white;
  1109. }
  1110. .nav-search {
  1111. float: left!important;
  1112. }
  1113. .anitracker-title-icon {
  1114. margin-left: 1rem!important;
  1115. opacity: .8!important;
  1116. color: #ff006c!important;
  1117. font-size: 2rem!important;
  1118. vertical-align: middle;
  1119. cursor: pointer;
  1120. padding: 0;
  1121. box-shadow: none!important;
  1122. }
  1123. .anitracker-title-icon:hover {
  1124. opacity: 1!important;
  1125. }
  1126. .anitracker-title-icon-check {
  1127. color: white;
  1128. margin-left: -.7rem!important;
  1129. font-size: 1rem!important;
  1130. vertical-align: super;
  1131. text-shadow: none;
  1132. opacity: 1!important;
  1133. }
  1134. .anitracker-header {
  1135. display: flex;
  1136. justify-content: left;
  1137. gap: 18px;
  1138. flex-grow: 0.05;
  1139. }
  1140. .anitracker-header-button {
  1141. color: white;
  1142. background: none;
  1143. border: 2px solid white;
  1144. border-radius: 5px;
  1145. width: 2rem;
  1146. }
  1147. .anitracker-header-button:hover {
  1148. border-color: #ff006c;
  1149. color: #ff006c;
  1150. }
  1151. .anitracker-header-button:focus {
  1152. border-color: #ff006c;
  1153. color: #ff006c;
  1154. }
  1155. .anitracker-header-notifications-circle {
  1156. color: rgb(255, 0, 108);
  1157. margin-left: -.3rem;
  1158. font-size: 0.7rem;
  1159. position: absolute;
  1160. }
  1161. .anitracker-notification-item .anitracker-main-text {
  1162. color: rgb(153, 153, 153);
  1163. }
  1164. .anitracker-notification-item-unwatched {
  1165. background-color: rgb(119, 62, 70);
  1166. }
  1167. .anitracker-notification-item-unwatched .anitracker-main-text {
  1168. color: white!important;
  1169. }
  1170. .anitracker-notification-item-unwatched .anitracker-subtext {
  1171. color: white!important;
  1172. }
  1173. .anitracker-watched-toggle {
  1174. font-size: 1.7em;
  1175. float: right;
  1176. margin-right: 5px;
  1177. margin-top: 5px;
  1178. cursor: pointer;
  1179. background-color: #592525;
  1180. padding: 5px;
  1181. border-radius: 5px;
  1182. }
  1183. .anitracker-watched-toggle:hover,.anitracker-watched-toggle:focus {
  1184. box-shadow: 0 0 0 .2rem rgb(255, 255, 255);
  1185. }
  1186. #anitracker-replace-cover {
  1187. z-index: 99;
  1188. right: 10px;
  1189. position: absolute;
  1190. bottom: 6em;
  1191. }
  1192. header.main-header nav .main-nav li.nav-item > a:focus {
  1193. color: #fff;
  1194. background-color: #bc0150;
  1195. }
  1196. .theatre-settings .dropup .btn:focus {
  1197. box-shadow: 0 0 0 .15rem rgb(100, 100, 100)!important;
  1198. }
  1199. .anitracker-episode-time {
  1200. margin-left: 5%;
  1201. font-size: 0.75rem!important;
  1202. cursor: default!important;
  1203. }
  1204. .anitracker-episode-time:hover {
  1205. text-decoration: none!important;
  1206. }
  1207. @media screen and (min-width: 1375px) {
  1208. .anitracker-theatre-mode {
  1209. max-width: 80%!important;
  1210. }
  1211. }
  1212. @keyframes anitracker-modalOpen {
  1213. 0% {
  1214. transform: scale(0.5);
  1215. }
  1216. 20% {
  1217. transform: scale(1.07);
  1218. }
  1219. 100% {
  1220. transform: scale(1);
  1221. }
  1222. }
  1223. @keyframes anitracker-fadeIn {
  1224. from {
  1225. opacity: 0;
  1226. }
  1227. to {
  1228. opacity: 1;
  1229. }
  1230. }
  1231. @keyframes anitracker-spin {
  1232. from {
  1233. transform: rotate(0deg);
  1234. }
  1235. to {
  1236. transform: rotate(360deg);
  1237. }
  1238. }
  1239. `.split(/^\}/mg).map(a => a.replace(/\n/gm,'') + '}');
  1240.  
  1241. for (let i = 0; i < rules.length - 1; i++) {
  1242. sheet.insertRule(rules[i], i);
  1243. }
  1244.  
  1245.  
  1246. const optionSwitches = [
  1247. {
  1248. optionId: 'autoDelete',
  1249. switchId: 'auto-delete',
  1250. value: initialStorage.autoDelete
  1251. },
  1252. {
  1253. optionId: 'theatreMode',
  1254. switchId: 'theatre-mode',
  1255. value: initialStorage.theatreMode,
  1256. onEvent: () => {
  1257. theatreMode(true);
  1258. },
  1259. offEvent: () => {
  1260. theatreMode(false);
  1261. }
  1262. },
  1263. {
  1264. optionId: 'hideThumbnails',
  1265. switchId: 'hide-thumbnails',
  1266. value: initialStorage.hideThumbnails,
  1267. onEvent: hideThumbnails,
  1268. offEvent: () => {
  1269. $('.main').removeClass('anitracker-hide-thumbnails');
  1270. }
  1271. },
  1272. {
  1273. optionId: 'bestQuality',
  1274. switchId: 'best-quality',
  1275. value: initialStorage.bestQuality,
  1276. onEvent: bestVideoQuality
  1277. },
  1278. {
  1279. optionId: 'autoDownload',
  1280. switchId: 'auto-download',
  1281. value: initialStorage.autoDownload
  1282. },
  1283. {
  1284. optionId: 'autoPlayNext',
  1285. switchId: 'autoplay-next',
  1286. value: initialStorage.autoPlayNext
  1287. },
  1288. {
  1289. optionId: 'autoPlayVideo',
  1290. switchId: 'autoplay-video',
  1291. value: initialStorage.autoPlayVideo
  1292. }];
  1293.  
  1294. const cachedAnimeData = [];
  1295.  
  1296. // Things that update when focusing this tab
  1297. $(document).on('visibilitychange', () => {
  1298. if (document.hidden) return;
  1299. updatePage();
  1300. });
  1301.  
  1302. function updatePage() {
  1303. updateSwitches();
  1304.  
  1305. const storage = getStorage();
  1306. const data = url.includes('/anime/') ? getAnimeData() : undefined;
  1307.  
  1308. if (data !== undefined) {
  1309. const isBookmarked = storage.bookmarks.find(a => a.id === data.id) !== undefined;
  1310. if (isBookmarked) $('.anitracker-bookmark-toggle .anitracker-title-icon-check').show();
  1311. else $('.anitracker-bookmark-toggle .anitracker-title-icon-check').hide();
  1312.  
  1313. const hasNotifications = storage.notifications.anime.find(a => a.id === data.id) !== undefined;
  1314. if (hasNotifications) $('.anitracker-notifications-toggle .anitracker-title-icon-check').show();
  1315. else $('.anitracker-notifications-toggle .anitracker-title-icon-check').hide();
  1316. }
  1317.  
  1318. if (!modalIsOpen() || $('.anitracker-view-notif-animes').length === 0) return;
  1319.  
  1320. for (const item of $('.anitracker-notification-item-unwatched')) {
  1321. const entry = storage.notifications.episodes.find(a => a.animeId === +$(item).attr('anime-data') && a.episode === +$(item).attr('episode-data') && a.watched === true);
  1322. if (entry === undefined) continue;
  1323. $(item).removeClass('anitracker-notification-item-unwatched');
  1324. const eye = $(item).find('.anitracker-watched-toggle');
  1325. eye.replaceClass('fa-eye', 'fa-eye-slash');
  1326. }
  1327. }
  1328.  
  1329. function theatreMode(on) {
  1330. if (on) $('.theatre>').addClass('anitracker-theatre-mode');
  1331. else $('.theatre>').removeClass('anitracker-theatre-mode');
  1332. }
  1333.  
  1334. function playAnimation(elem, anim, type = '', duration) {
  1335. return new Promise(resolve => {
  1336. elem.css('animation', `anitracker-${anim} ${duration || animationTimes[anim]}s forwards linear ${type}`);
  1337. if (animationTimes[anim] === undefined) resolve();
  1338. setTimeout(() => {
  1339. elem.css('animation', '');
  1340. resolve();
  1341. }, animationTimes[anim] * 1000);
  1342. });
  1343. }
  1344.  
  1345. let modalCloseFunction = closeModal;
  1346. // AnimePahe Improvements modal
  1347. function addModal() {
  1348. $(`
  1349. <div id="anitracker-modal" tabindex="-1">
  1350. <div id="anitracker-modal-content">
  1351. <i id="anitracker-modal-close" class="fa fa-close" title="Close modal">
  1352. </i>
  1353. <div id="anitracker-modal-body"></div>
  1354. </div>
  1355. </div>`).insertBefore('.main-header');
  1356.  
  1357. $('#anitracker-modal').on('click', (e) => {
  1358. if (e.target !== e.currentTarget) return;
  1359. modalCloseFunction();
  1360. });
  1361.  
  1362. $('#anitracker-modal-close').on('click', () => {
  1363. modalCloseFunction();
  1364. });
  1365. }
  1366. addModal();
  1367.  
  1368. function openModal(closeFunction = closeModal) {
  1369. if (closeFunction !== closeModal) $('#anitracker-modal-close').replaceClass('fa-close', 'fa-arrow-left');
  1370. else $('#anitracker-modal-close').replaceClass('fa-arrow-left', 'fa-close');
  1371.  
  1372. return new Promise(resolve => {
  1373. playAnimation($('#anitracker-modal-content'), 'modalOpen');
  1374. playAnimation($('#anitracker-modal'), 'fadeIn').then(() => {
  1375. $('#anitracker-modal').focus();
  1376. resolve();
  1377. });
  1378. $('#anitracker-modal').css('display','flex');
  1379. modalCloseFunction = closeFunction;
  1380. });
  1381. }
  1382.  
  1383. function closeModal() {
  1384. if ($('#anitracker-modal').css('animation') !== 'none') {
  1385. $('#anitracker-modal').hide();
  1386. return;
  1387. }
  1388.  
  1389. playAnimation($('#anitracker-modal'), 'fadeIn', 'reverse', 0.1).then(() => {
  1390. $('#anitracker-modal').hide();
  1391. });
  1392. }
  1393.  
  1394. function modalIsOpen() {
  1395. return $('#anitracker-modal').is(':visible');
  1396. }
  1397.  
  1398. let currentEpisodeTime = 0;
  1399. // Messages received from iframe
  1400. if (isEpisode()) {
  1401. window.onmessage = function(e) {
  1402. const data = e.data;
  1403.  
  1404. if (typeof(data) === 'number') {
  1405. currentEpisodeTime = Math.trunc(data);
  1406. return;
  1407. }
  1408.  
  1409. const action = data.action;
  1410. if (action === 'id_request') {
  1411. sendMessage({action:"id_response",id:getAnimeData().id});
  1412. }
  1413. else if (action === 'video_url_request') {
  1414. const selected = {
  1415. src: undefined,
  1416. res: undefined,
  1417. audio: undefined
  1418. }
  1419. for (const btn of $('#resolutionMenu>button')) {
  1420. const src = $(btn).data('src');
  1421. const res = +$(btn).data('resolution');
  1422. const audio = $(btn).data('audio');
  1423. if (selected.src !== undefined && selected.res < res) continue;
  1424. if (selected.audio !== undefined && audio === 'jp' && selected.res <= res) continue; // Prefer dubs, since they don't have subtitles
  1425. selected.src = src;
  1426. selected.res = res;
  1427. selected.audio = audio;
  1428. }
  1429. if (selected.src === undefined) {
  1430. console.error("[AnimePahe Improvements] Didn't find video URL");
  1431. return;
  1432. }
  1433. console.log('[AnimePahe Improvements] Found lowest resolution URL ' + selected.src);
  1434. sendMessage({action:"video_url_response", url:selected.src});
  1435. }
  1436. else if (action === 'key') {
  1437. if (data.key === 't') {
  1438. toggleTheatreMode();
  1439. }
  1440. }
  1441. else if (data === 'ended') {
  1442. const storage = getStorage();
  1443. if (storage.autoPlayNext !== true) return;
  1444. const elem = $('.sequel a');
  1445. if (elem.length > 0) elem[0].click();
  1446. }
  1447. else if (action === 'next') {
  1448. const elem = $('.sequel a');
  1449. if (elem.length > 0) elem[0].click();
  1450. }
  1451. else if (action === 'previous') {
  1452. const elem = $('.prequel a');
  1453. if (elem.length > 0) elem[0].click();
  1454. }
  1455. };
  1456. }
  1457.  
  1458. function sendMessage(message) {
  1459. $('.embed-responsive-item')[0].contentWindow.postMessage(message,'*');
  1460. }
  1461.  
  1462. function toggleTheatreMode() {
  1463. const storage = getStorage();
  1464. theatreMode(!storage.theatreMode);
  1465.  
  1466. storage.theatreMode = !storage.theatreMode;
  1467. saveData(storage);
  1468. updateSwitches();
  1469. }
  1470.  
  1471. function getSeasonValue(season) {
  1472. return ({winter:0, spring:1, summer:2, fall:3})[season.toLowerCase()];
  1473. }
  1474.  
  1475. function getSeasonName(season) {
  1476. return ["winter","spring","summer","fall"][season];
  1477. }
  1478.  
  1479. function stringSimilarity(s1, s2) {
  1480. let longer = s1;
  1481. let shorter = s2;
  1482. if (s1.length < s2.length) {
  1483. longer = s2;
  1484. shorter = s1;
  1485. }
  1486. const longerLength = longer.length;
  1487. if (longerLength == 0) {
  1488. return 1.0;
  1489. }
  1490. return (longerLength - editDistance(longer, shorter)) / parseFloat(longerLength);
  1491. }
  1492.  
  1493. function editDistance(s1, s2) {
  1494. s1 = s1.toLowerCase();
  1495. s2 = s2.toLowerCase();
  1496. const costs = [];
  1497. for (let i = 0; i <= s1.length; i++) {
  1498. let lastValue = i;
  1499. for (let j = 0; j <= s2.length; j++) {
  1500. if (i == 0)
  1501. costs[j] = j;
  1502. else {
  1503. if (j > 0) {
  1504. let newValue = costs[j - 1];
  1505. if (s1.charAt(i - 1) != s2.charAt(j - 1))
  1506. newValue = Math.min(Math.min(newValue, lastValue),
  1507. costs[j]) + 1;
  1508. costs[j - 1] = lastValue;
  1509. lastValue = newValue;
  1510. }
  1511. }
  1512. }
  1513. if (i > 0)
  1514. costs[s2.length] = lastValue;
  1515. }
  1516. return costs[s2.length];
  1517. }
  1518.  
  1519. function searchForCollections() {
  1520. if ($('.search-results a').length === 0) return;
  1521.  
  1522. const baseName = $($('.search-results .result-title')[0]).text();
  1523.  
  1524. const request = new XMLHttpRequest();
  1525. request.open('GET', '/api?m=search&q=' + encodeURIComponent(baseName), true);
  1526.  
  1527. request.onload = () => {
  1528. if (request.readyState !== 4 || request.status !== 200 ) return;
  1529.  
  1530. response = JSON.parse(request.response).data;
  1531.  
  1532. if (response == undefined) return;
  1533.  
  1534. let seriesList = [];
  1535.  
  1536. for (const anime of response) {
  1537. if (stringSimilarity(baseName, anime.title) >= 0.42 || (anime.title.startsWith(baseName) && stringSimilarity(baseName, anime.title) >= 0.25)) {
  1538. seriesList.push(anime);
  1539. }
  1540. }
  1541.  
  1542. if (seriesList.length < 2) return;
  1543. seriesList = sortAnimesChronologically(seriesList);
  1544.  
  1545. displayCollection(baseName, seriesList);
  1546. };
  1547.  
  1548. request.send();
  1549. }
  1550.  
  1551. new MutationObserver(function(mutationList, observer) {
  1552. if (!searchComplete()) return;
  1553. searchForCollections();
  1554. }).observe($('.search-results-wrap')[0], { childList: true });
  1555.  
  1556. function searchComplete() {
  1557. return $('.search-results').length !== 0 && $('.search-results a').length > 0;
  1558. }
  1559.  
  1560. function displayCollection(baseName, seriesList) {
  1561. $(`
  1562. <li class="anitracker-collection" data-index="-1">
  1563. <a title="${toHtmlCodes(baseName + " - Collection")}" href="javascript:;">
  1564. <img src="${seriesList[0].poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" style="pointer-events: all !important;max-width: 30px;">
  1565. <img src="${seriesList[1].poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" style="pointer-events: all !important;max-width: 30px;left:30px;">
  1566. <div class="result-title">${baseName}</div>
  1567. <div class="result-status"><strong>Collection</strong> - ${seriesList.length} Entries</div>
  1568. </a>
  1569. </li>`).prependTo('.search-results');
  1570.  
  1571. function displayInModal() {
  1572. $('#anitracker-modal-body').empty();
  1573. $(`
  1574. <h4>Collection</h4>
  1575. <div class="anitracker-modal-list-container">
  1576. <div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;"></div>
  1577. </div>`).appendTo('#anitracker-modal-body');
  1578.  
  1579. for (const anime of seriesList) {
  1580. $(`
  1581. <div class="anitracker-big-list-item anitracker-collection-item">
  1582. <a href="/anime/${anime.session}" title="${toHtmlCodes(anime.title)}">
  1583. <img src="${anime.poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" alt="[Thumbnail of ${anime.title}]">
  1584. <div class="anitracker-main-text">${anime.title}</div>
  1585. <div class="anitracker-subtext"><strong>${anime.type}</strong> - ${anime.episodes > 0 ? anime.episodes : '?'} Episode${anime.episodes === 1 ? '' : 's'} (${anime.status})</div>
  1586. <div class="anitracker-subtext">${anime.season} ${anime.year}</div>
  1587. </a>
  1588. </div>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
  1589. }
  1590.  
  1591. openModal();
  1592. }
  1593.  
  1594. $('.anitracker-collection').on('click', displayInModal);
  1595. $('.input-search').on('keyup', (e) => {
  1596. if (e.key === "Enter" && $('.anitracker-collection').hasClass('selected')) displayInModal();
  1597. });
  1598. }
  1599.  
  1600. function getSeasonTimeframe(from, to) {
  1601. const filters = [];
  1602. for (let i = from.year; i <= to.year; i++) {
  1603. const start = i === from.year ? from.season : 0;
  1604. const end = i === to.year ? to.season : 3;
  1605. for (let d = start; d <= end; d++) {
  1606. filters.push(`season/${getSeasonName(d)}-${i.toString()}`);
  1607. }
  1608. }
  1609. return filters;
  1610. }
  1611.  
  1612. const is404 = $('h1').text().includes('404');
  1613.  
  1614. if (!isRandomAnime() && initialStorage.cache !== undefined) {
  1615. const storage = getStorage();
  1616. delete storage.cache;
  1617. saveData(storage);
  1618. }
  1619.  
  1620. const filterSearchCache = {};
  1621.  
  1622. const filterValues = {
  1623. "genre":[
  1624. {"name":"Comedy","value":"comedy"},{"name":"Slice of Life","value":"slice-of-life"},{"name":"Romance","value":"romance"},{"name":"Ecchi","value":"ecchi"},{"name":"Drama","value":"drama"},
  1625. {"name":"Supernatural","value":"supernatural"},{"name":"Sports","value":"sports"},{"name":"Horror","value":"horror"},{"name":"Sci-Fi","value":"sci-fi"},{"name":"Action","value":"action"},
  1626. {"name":"Fantasy","value":"fantasy"},{"name":"Mystery","value":"mystery"},{"name":"Suspense","value":"suspense"},{"name":"Adventure","value":"adventure"},{"name":"Boys Love","value":"boys-love"},
  1627. {"name":"Girls Love","value":"girls-love"},{"name":"Hentai","value":"hentai"},{"name":"Gourmet","value":"gourmet"},{"name":"Erotica","value":"erotica"},{"name":"Avant Garde","value":"avant-garde"},
  1628. {"name":"Award Winning","value":"award-winning"}
  1629. ],
  1630. "theme":[
  1631. {"name":"Adult Cast","value":"adult-cast"},{"name":"Anthropomorphic","value":"anthropomorphic"},{"name":"Detective","value":"detective"},{"name":"Love Polygon","value":"love-polygon"},
  1632. {"name":"Mecha","value":"mecha"},{"name":"Music","value":"music"},{"name":"Psychological","value":"psychological"},{"name":"School","value":"school"},{"name":"Super Power","value":"super-power"},
  1633. {"name":"Space","value":"space"},{"name":"CGDCT","value":"cgdct"},{"name":"Romantic Subtext","value":"romantic-subtext"},{"name":"Historical","value":"historical"},{"name":"Video Game","value":"video-game"},
  1634. {"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"},
  1635. {"name":"Performing Arts","value":"performing-arts"},{"name":"Military","value":"military"},{"name":"Harem","value":"harem"},{"name":"Reverse Harem","value":"reverse-harem"},{"name":"Samurai","value":"samurai"},
  1636. {"name":"Vampire","value":"vampire"},{"name":"Mythology","value":"mythology"},{"name":"High Stakes Game","value":"high-stakes-game"},{"name":"Strategy Game","value":"strategy-game"},
  1637. {"name":"Magical Sex Shift","value":"magical-sex-shift"},{"name":"Racing","value":"racing"},{"name":"Isekai","value":"isekai"},{"name":"Workplace","value":"workplace"},{"name":"Iyashikei","value":"iyashikei"},
  1638. {"name":"Time Travel","value":"time-travel"},{"name":"Gore","value":"gore"},{"name":"Educational","value":"educational"},{"name":"Delinquents","value":"delinquents"},{"name":"Organized Crime","value":"organized-crime"},
  1639. {"name":"Otaku Culture","value":"otaku-culture"},{"name":"Medical","value":"medical"},{"name":"Survival","value":"survival"},{"name":"Reincarnation","value":"reincarnation"},{"name":"Showbiz","value":"showbiz"},
  1640. {"name":"Team Sports","value":"team-sports"},{"name":"Mahou Shoujo","value":"mahou-shoujo"},{"name":"Combat Sports","value":"combat-sports"},{"name":"Crossdressing","value":"crossdressing"},
  1641. {"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"},
  1642. {"name":"Villainess","value":"villainess"}
  1643. ],
  1644. "type":[
  1645. {"name":"TV","value":"tv"},{"name":"Movie","value":"movie"},{"name":"OVA","value":"ova"},{"name":"ONA","value":"ona"},{"name":"Special","value":"special"},{"name":"Music","value":"music"}
  1646. ],
  1647. "demographic":[
  1648. {"name":"Shounen","value":"shounen"},{"name":"Shoujo","value":"shoujo"},{"name":"Seinen","value":"seinen"},{"name":"Kids","value":"kids"},{"name":"Josei","value":"josei"}
  1649. ],
  1650. "":[
  1651. {"value":"airing"},{"value":"completed"}
  1652. ]
  1653. };
  1654.  
  1655. const filterRules = {
  1656. genre: "and",
  1657. theme: "and",
  1658. demographic: "or",
  1659. type: "or",
  1660. season: "or",
  1661. "": "or"
  1662. };
  1663.  
  1664. function getFilterParts(filter) {
  1665. const regex = /^(?:([\w\-]+)(?:\/))?([\w\-\.]+)$/.exec(filter);
  1666. return {
  1667. type: regex[1] || '',
  1668. value: regex[2]
  1669. };
  1670. }
  1671.  
  1672. function buildFilterString(type, value) {
  1673. return (type === '' ? type : type + '/') + value;
  1674. }
  1675.  
  1676. const seasonFilterRegex = /^season\/(spring|summer|winter|fall)-\d{4}\.\.(spring|summer|winter|fall)-\d{4}$/;
  1677. const noneFilterRegex = /^([\w\d\-]+\/)?none$/;
  1678.  
  1679. function getFilteredList(filtersInput, filterTotal = 0) {
  1680. let filterNum = 0;
  1681.  
  1682. function getPage(pageUrl) {
  1683. return new Promise((resolve, reject) => {
  1684. const cached = filterSearchCache[pageUrl];
  1685. if (cached !== undefined) { // If cache exists
  1686. if (cached === 'invalid') {
  1687. resolve(undefined);
  1688. return;
  1689. }
  1690. resolve(cached);
  1691. return;
  1692. }
  1693. const req = new XMLHttpRequest();
  1694. req.open('GET', pageUrl, true);
  1695. try {
  1696. req.send();
  1697. }
  1698. catch (err) {
  1699. console.error(err);
  1700. reject('A network error occured.');
  1701. return;
  1702. }
  1703.  
  1704. req.onload = () => {
  1705. if (req.status !== 200) {
  1706. resolve(undefined);
  1707. return;
  1708. }
  1709. const animeList = getAnimeList($(req.response));
  1710. filterSearchCache[pageUrl] = animeList;
  1711. resolve(animeList);
  1712. };
  1713. });
  1714. }
  1715.  
  1716. function getLists(filters) {
  1717. const lists = [];
  1718.  
  1719. return new Promise((resolve, reject) => {
  1720. function check() {
  1721. if (filters.length > 0) {
  1722. repeat(filters.shift());
  1723. }
  1724. else {
  1725. resolve(lists);
  1726. }
  1727. }
  1728.  
  1729. function repeat(filter) {
  1730. const filterType = getFilterParts(filter).type;
  1731. if (noneFilterRegex.test(filter)) {
  1732. getLists(filterValues[filterType].map(a => buildFilterString(filterType, a.value))).then((filtered) => {
  1733. getPage('/anime').then((unfiltered) => {
  1734. const none = [];
  1735. for (const entry of unfiltered) {
  1736. if (filtered.find(list => list.entries.find(a => a.name === entry.name)) !== undefined) continue;
  1737. none.push(entry);
  1738. }
  1739.  
  1740. lists.push({
  1741. type: filterType,
  1742. entries: none
  1743. });
  1744.  
  1745. check();
  1746. });
  1747. });
  1748. return;
  1749. }
  1750. getPage('/anime/' + filter).then((result) => {
  1751. if (result !== undefined) {
  1752. lists.push({
  1753. type: filterType,
  1754. entries: result
  1755. });
  1756. }
  1757. if (filterTotal > 0) {
  1758. filterNum++;
  1759. $($('.anitracker-filter-spinner>span')[0]).text(Math.floor((filterNum/filterTotal) * 100).toString() + '%');
  1760. }
  1761.  
  1762. check();
  1763. });
  1764. }
  1765.  
  1766. check();
  1767. });
  1768. }
  1769.  
  1770. return new Promise((resolve, reject) => {
  1771. const filters = JSON.parse(JSON.stringify(filtersInput));
  1772.  
  1773. if (filters.length === 0) {
  1774. getPage('/anime').then((response) => {
  1775. if (response === undefined) {
  1776. alert('Page loading failed.');
  1777. reject('Anime index page not reachable.');
  1778. return;
  1779. }
  1780.  
  1781. resolve(response);
  1782. });
  1783. return;
  1784. }
  1785.  
  1786. const seasonFilter = filters.find(a => seasonFilterRegex.test(a));
  1787. if (seasonFilter !== undefined) {
  1788. filters.splice(filters.indexOf(seasonFilter), 1);
  1789. const range = getFilterParts(seasonFilter).value.split('..');
  1790. filters.push(...getSeasonTimeframe({
  1791. year: +range[0].split('-')[1],
  1792. season: getSeasonValue(range[0].split('-')[0])
  1793. },
  1794. {
  1795. year: +range[1].split('-')[1],
  1796. season: getSeasonValue(range[1].split('-')[0])
  1797. }));
  1798. }
  1799.  
  1800. getLists(filters).then((listsInput) => {
  1801. const lists = JSON.parse(JSON.stringify(listsInput));
  1802. const types = {};
  1803. for (const list of lists) {
  1804. if (types[list.type]) continue;
  1805. types[list.type] = list.entries;
  1806. }
  1807. lists.splice(0, 1);
  1808.  
  1809. for (const list of lists) {
  1810. const entries = list.entries;
  1811. if (filterRules[list.type] === 'and') {
  1812. const matches = [];
  1813. for (const anime of types[list.type]) {
  1814. if (entries.find(a => a.name === anime.name) === undefined) continue;
  1815. matches.push(anime);
  1816. }
  1817. types[list.type] = matches;
  1818. }
  1819. else if (filterRules[list.type] === 'or') {
  1820. for (const anime of list.entries) {
  1821. if (types[list.type].find(a => a.name === anime.name) !== undefined) continue;
  1822. types[list.type].push(anime);
  1823. }
  1824. }
  1825. }
  1826.  
  1827. const listOfTypes = Array.from(Object.values(types));
  1828. let finalList = listOfTypes[0];
  1829. listOfTypes.splice(0,1);
  1830.  
  1831. for (const type of listOfTypes) {
  1832. const matches = [];
  1833. for (const anime of type) {
  1834. if (finalList.find(a => a.name === anime.name) === undefined) continue;
  1835. matches.push(anime);
  1836. }
  1837. finalList = matches;
  1838. }
  1839. resolve(finalList);
  1840. });
  1841. });
  1842. }
  1843.  
  1844. function searchList(fuseClass, list, query, limit = 80) {
  1845. const fuse = new fuseClass(list, {
  1846. keys: ['name'],
  1847. findAllMatches: true
  1848. });
  1849.  
  1850. const matching = fuse.search(query);
  1851.  
  1852. return matching.map(a => {return a.item}).splice(0,limit);
  1853. }
  1854.  
  1855. function timeSince(date) {
  1856.  
  1857. var seconds = Math.floor((new Date() - date) / 1000);
  1858.  
  1859. var interval = Math.floor(seconds / 31536000);
  1860.  
  1861. if (interval >= 1) {
  1862. return interval + " year" + (interval > 1 ? 's' : '');
  1863. }
  1864. interval = Math.floor(seconds / 2592000);
  1865. if (interval >= 1) {
  1866. return interval + " month" + (interval > 1 ? 's' : '');
  1867. }
  1868. interval = Math.floor(seconds / 86400);
  1869. if (interval >= 1) {
  1870. return interval + " day" + (interval > 1 ? 's' : '');
  1871. }
  1872. interval = Math.floor(seconds / 3600);
  1873. if (interval >= 1) {
  1874. return interval + " hour" + (interval > 1 ? 's' : '');
  1875. }
  1876. interval = Math.floor(seconds / 60);
  1877. if (interval >= 1) {
  1878. return interval + " minute" + (interval > 1 ? 's' : '');
  1879. }
  1880. return seconds + " second" + (seconds > 1 ? 's' : '');
  1881. }
  1882.  
  1883. if (window.location.pathname.startsWith('/customlink')) {
  1884. const parts = {
  1885. animeSession: '',
  1886. episodeSession: '',
  1887. time: -1
  1888. };
  1889. const entries = Array.from(new URLSearchParams(window.location.search).entries()).sort((a,b) => a[0] > b[0] ? 1 : -1);
  1890. for (const entry of entries) {
  1891. if (entry[0] === 'a') {
  1892. parts.animeSession = getAnimeData(decodeURIComponent(entry[1])).session;
  1893. continue;
  1894. }
  1895. if (entry[0] === 'e') {
  1896. if (parts.animeSession === '') return;
  1897. parts.episodeSession = getEpisodeSession(parts.animeSession, +entry[1]);
  1898. continue;
  1899. }
  1900. if (entry[0] === 't') {
  1901. if (parts.animeSession === '') return;
  1902. if (parts.episodeSession === '') continue;
  1903.  
  1904. parts.time = +entry[1];
  1905. continue;
  1906. }
  1907. }
  1908.  
  1909. const destination = (() => {
  1910. if (parts.animeSession !== '' && parts.episodeSession === '' && parts.time === -1) {
  1911. return '/anime/' + parts.animeSession + '?ref=customlink';
  1912. }
  1913. if (parts.animeSession !== '' && parts.episodeSession !== '' && parts.time === -1) {
  1914. return '/play/' + parts.animeSession + '/' + parts.episodeSession + '?ref=customlink';
  1915. }
  1916. if (parts.animeSession !== '' && parts.episodeSession !== '' && parts.time >= 0) {
  1917. return '/play/' + parts.animeSession + '/' + parts.episodeSession + '?time=' + parts.time + '&ref=customlink';
  1918. }
  1919. return undefined;
  1920. })();
  1921.  
  1922. if (destination !== undefined) {
  1923. document.title = "Redirecting... :: animepahe";
  1924. $('h1').text('Redirecting...');
  1925. window.location.replace(destination);
  1926. }
  1927.  
  1928. return;
  1929. }
  1930.  
  1931. // Main key events
  1932. if (!is404) $(document).on('keydown', (e) => {
  1933. if ($(e.target).is(':input')) return;
  1934.  
  1935. if (modalIsOpen() && ['Escape','Backspace'].includes(e.key)) {
  1936. modalCloseFunction();
  1937. return;
  1938. }
  1939. if (!isEpisode() || modalIsOpen()) return;
  1940. if (e.key === 't') {
  1941. toggleTheatreMode();
  1942. }
  1943. else {
  1944. sendMessage({action:"key",key:e.key});
  1945. $('.embed-responsive-item')[0].contentWindow.focus();
  1946. if ([" "].includes(e.key)) e.preventDefault();
  1947. }
  1948. });
  1949.  
  1950. if (window.location.pathname.startsWith('/queue')) {
  1951. $(`
  1952. <span style="font-size:.6em;">&nbsp;&nbsp;&nbsp;(Incoming episodes)</span>
  1953. `).appendTo('h2');
  1954. }
  1955.  
  1956. if (/^\/anime\/\w+(\/[\w\-\.]+)?$/.test(window.location.pathname)) {
  1957. if (is404) return;
  1958.  
  1959. const filter = /\/anime\/([^\/]+)\/?([^\/]+)?/.exec(window.location.pathname);
  1960.  
  1961. if (filter[2] !== undefined) {
  1962. if (filterRules[filter[1]] === undefined) return;
  1963. if (filter[1] === 'season') {
  1964. window.location.replace(`/anime?${filter[1]}=${filter[2]}..${filter[2]}`);
  1965. return;
  1966. }
  1967. window.location.replace(`/anime?${filter[1]}=${filter[2]}`);
  1968. }
  1969. else {
  1970. window.location.replace(`/anime?other=${filter[1]}`);
  1971. }
  1972. return;
  1973. }
  1974.  
  1975. function getDayName(day) {
  1976. return [
  1977. "Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"
  1978. ][day];
  1979. }
  1980.  
  1981. function toHtmlCodes(string) {
  1982. return $('<div>').text(string).html().replace(/"/g, "&quot;").replace(/'/g, "&#039;");
  1983. }
  1984.  
  1985. // Bookmark & episode feed header buttons
  1986. $(`
  1987. <div class="anitracker-header">
  1988. <button class="anitracker-header-notifications anitracker-header-button" title="View episode feed">
  1989. <i class="fa fa-bell" aria-hidden="true"></i>
  1990. <i style="display:none;" aria-hidden="true" class="fa fa-circle anitracker-header-notifications-circle"></i>
  1991. </button>
  1992. <button class="anitracker-header-bookmark anitracker-header-button" title="View bookmarks"><i class="fa fa-bookmark" aria-hidden="true"></i></button>
  1993. </div>`).insertAfter('.navbar-nav');
  1994.  
  1995. let currentNotificationIndex = 0;
  1996.  
  1997. function openNotificationsModal() {
  1998. currentNotificationIndex = 0;
  1999. const oldStorage = getStorage();
  2000. $('#anitracker-modal-body').empty();
  2001.  
  2002. $(`
  2003. <h4>Episode Feed</h4>
  2004. <div class="btn-group" style="margin-bottom: 10px;">
  2005. <button class="btn btn-secondary anitracker-view-notif-animes">
  2006. Handle Feed...
  2007. </button>
  2008. </div>
  2009. <div class="anitracker-modal-list-container">
  2010. <div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;">
  2011. <div id="anitracker-notifications-list-spinner" style="display:flex;justify-content:center;">
  2012. <div class="spinner-border text-danger" role="status">
  2013. <span class="sr-only">Loading...</span>
  2014. </div>
  2015. </div>
  2016. </div>
  2017. </div>`).appendTo('#anitracker-modal-body');
  2018.  
  2019. $('.anitracker-view-notif-animes').on('click', () => {
  2020. $('#anitracker-modal-body').empty();
  2021. const storage = getStorage();
  2022. $(`
  2023. <h4>Handle Episode Feed</h4>
  2024. <div class="anitracker-modal-list-container">
  2025. <div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;"></div>
  2026. </div>
  2027. `).appendTo('#anitracker-modal-body');
  2028. [...storage.notifications.anime].sort((a,b) => a.latest_episode > b.latest_episode ? 1 : -1).forEach(g => {
  2029. const latestEp = new Date(g.latest_episode + " UTC");
  2030. const latestEpString = g.latest_episode !== undefined ? `${getDayName(latestEp.getDay())} ${latestEp.toLocaleTimeString()}` : "None found";
  2031. $(`
  2032. <div class="anitracker-modal-list-entry" animeid="${g.id}" animename="${toHtmlCodes(g.name)}">
  2033. <a href="/a/${g.id}" target="_blank" title="${toHtmlCodes(g.name)}">
  2034. ${g.name}
  2035. </a><br>
  2036. <span>
  2037. Latest episode: ${latestEpString}
  2038. </span><br>
  2039. <div class="btn-group">
  2040. <button class="btn btn-secondary anitracker-get-all-button" title="Put all episodes in the feed" ${g.hasFirstEpisode ? 'disabled=""' : ''}>
  2041. <i class="fa fa-rotate-right" aria-hidden="true"></i>
  2042. &nbsp;Get All
  2043. </button>
  2044. </div>
  2045. <div class="btn-group">
  2046. <button class="btn btn-danger anitracker-delete-button" title="Remove this anime from the episode feed">
  2047. <i class="fa fa-trash" aria-hidden="true"></i>
  2048. &nbsp;Remove
  2049. </button>
  2050. </div>
  2051. </div>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
  2052. });
  2053. if (storage.notifications.anime.length === 0) {
  2054. $("<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');
  2055. }
  2056.  
  2057. $('.anitracker-modal-list-entry .anitracker-get-all-button').on('click', (e) => {
  2058. const elem = $(e.currentTarget);
  2059. const id = +elem.parents().eq(1).attr('animeid');
  2060. const storage = getStorage();
  2061.  
  2062. const found = storage.notifications.anime.find(a => a.id === id);
  2063. if (found === undefined) {
  2064. console.error("[AnimePahe Improvements] Couldn't find feed for anime with id " + id);
  2065. return;
  2066. }
  2067.  
  2068. found.hasFirstEpisode = true;
  2069. found.updateFrom = 0;
  2070. saveData(storage);
  2071.  
  2072. elem.replaceClass("btn-secondary", "btn-primary");
  2073. setTimeout(() => {
  2074. elem.replaceClass("btn-primary", "btn-secondary");
  2075. elem.prop('disabled', true);
  2076. }, 200);
  2077. });
  2078.  
  2079. $('.anitracker-modal-list-entry .anitracker-delete-button').on('click', (e) => {
  2080. const parent = $(e.currentTarget).parents().eq(1);
  2081. const name = parent.attr('animename');
  2082. toggleNotifications(name, +parent.attr('animeid'));
  2083.  
  2084. const name2 = getAnimeName();
  2085. if (name2.length > 0 && name2 === name) {
  2086. $('.anitracker-notifications-toggle .anitracker-title-icon-check').hide();
  2087. }
  2088.  
  2089. parent.remove();
  2090. });
  2091.  
  2092. openModal(openNotificationsModal);
  2093. });
  2094.  
  2095. const animeData = [];
  2096. const queue = [...oldStorage.notifications.anime];
  2097.  
  2098. openModal().then(() => {
  2099. if (queue.length > 0) next();
  2100. else done();
  2101. });
  2102.  
  2103. async function next() {
  2104. if (queue.length === 0) done();
  2105. const anime = queue.shift();
  2106. const data = await updateNotifications(anime.name);
  2107.  
  2108. if (data === -1) {
  2109. $("<span>An error occured.</span>").appendTo('#anitracker-modal-body .anitracker-modal-list');
  2110. return;
  2111. }
  2112. animeData.push({
  2113. id: anime.id,
  2114. data: data
  2115. });
  2116.  
  2117. if (queue.length > 0 && $('#anitracker-notifications-list-spinner').length > 0) next();
  2118. else done();
  2119. }
  2120.  
  2121. function done() {
  2122. if ($('#anitracker-notifications-list-spinner').length === 0) return;
  2123. const storage = getStorage();
  2124. let removedAnime = 0;
  2125. for (const anime of storage.notifications.anime) {
  2126. if (anime.latest_episode === undefined || anime.dont_ask === true) continue;
  2127. const time = Date.now() - new Date(anime.latest_episode + " UTC").getTime();
  2128. if ((time / 1000 / 60 / 60 / 24 / 7) > 2) {
  2129. 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.`);
  2130. if (remove === true) {
  2131. toggleNotifications(anime.name, anime.id);
  2132. removedAnime++;
  2133. }
  2134. else {
  2135. anime.dont_ask = true;
  2136. saveData(storage);
  2137. }
  2138. }
  2139. }
  2140. if (removedAnime > 0) {
  2141. openNotificationsModal();
  2142. return;
  2143. }
  2144. $('#anitracker-notifications-list-spinner').remove();
  2145. storage.notifications.episodes.sort((a,b) => a.time < b.time ? 1 : -1);
  2146. storage.notifications.lastUpdated = Date.now();
  2147. saveData(storage);
  2148. if (storage.notifications.episodes.length === 0) {
  2149. $("<span>Nothing here yet!</span>").appendTo('#anitracker-modal-body .anitracker-modal-list');
  2150. }
  2151. else addToList(20);
  2152. }
  2153.  
  2154. function addToList(num) {
  2155. const storage = getStorage();
  2156. const index = currentNotificationIndex;
  2157. for (let i = currentNotificationIndex; i < storage.notifications.episodes.length; i++) {
  2158. const ep = storage.notifications.episodes[i];
  2159. if (ep === undefined) break;
  2160. currentNotificationIndex++;
  2161. const data = animeData.find(a => a.id === ep.animeId)?.data;
  2162. if (data === undefined) {
  2163. console.error(`[AnimePahe Improvements] Could not find corresponding anime "${ep.animeName}" with ID ${ep.animeId} (episode ${ep.episode})`);
  2164. continue;
  2165. }
  2166.  
  2167. const releaseTime = new Date(ep.time + " UTC");
  2168. $(`
  2169. <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}">
  2170. <a href="/play/${data.session}/${ep.session}" target="_blank" title="${toHtmlCodes(data.title)}">
  2171. <img src="${data.poster.slice(0, -3) + 'th.jpg'}" referrerpolicy="no-referrer" alt="[Thumbnail of ${toHtmlCodes(data.title)}]"}>
  2172. <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>
  2173. <div class="anitracker-main-text">${data.title}</div>
  2174. <div class="anitracker-subtext"><strong>Episode ${ep.episode}</strong></div>
  2175. <div class="anitracker-subtext">${timeSince(releaseTime)} ago (${releaseTime.toLocaleDateString()})</div>
  2176. </a>
  2177. </div>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
  2178. if (i > index+num-1) break;
  2179. }
  2180.  
  2181. $('.anitracker-notification-item.anitracker-temp').on('click', (e) => {
  2182. $(e.currentTarget).find('a').blur();
  2183. });
  2184.  
  2185. $('.anitracker-notification-item.anitracker-temp .anitracker-watched-toggle').on('click keydown', (e) => {
  2186. if (e.type === 'keydown' && e.key !== "Enter") return;
  2187. e.preventDefault();
  2188. const storage = getStorage();
  2189. const elem = $(e.currentTarget);
  2190. const parent = elem.parents().eq(1);
  2191. const ep = storage.notifications.episodes.find(a => a.animeId === +parent.attr('anime-data') && a.episode === +parent.attr('episode-data'));
  2192. if (ep === undefined) {
  2193. console.error("[AnimePahe Improvements] couldn't mark episode as watched/unwatched");
  2194. return;
  2195. }
  2196. parent.toggleClass('anitracker-notification-item-unwatched');
  2197. elem.toggleClass('fa-eye').toggleClass('fa-eye-slash');
  2198.  
  2199. if (e.type === 'click') elem.blur();
  2200.  
  2201. ep.watched = !ep.watched;
  2202. elem.attr('title', `Mark this episode as ${ep.watched ? 'unwatched' : 'watched'}`);
  2203.  
  2204. saveData(storage);
  2205. });
  2206.  
  2207. $('.anitracker-notification-item.anitracker-temp').removeClass('anitracker-temp');
  2208.  
  2209. }
  2210.  
  2211. $('#anitracker-modal-body').on('scroll', () => {
  2212. const elem = $('#anitracker-modal-body');
  2213. if (elem.scrollTop() >= elem[0].scrollTopMax) {
  2214. if ($('.anitracker-view-notif-animes').length === 0) return;
  2215. addToList(20);
  2216. }
  2217. });
  2218. }
  2219.  
  2220. $('.anitracker-header-notifications').on('click', openNotificationsModal);
  2221.  
  2222. $('.anitracker-header-bookmark').on('click', () => {
  2223. $('#anitracker-modal-body').empty();
  2224. const storage = getStorage();
  2225. $(`
  2226. <h4>Bookmarks</h4>
  2227. <div class="anitracker-modal-list-container">
  2228. <div class="anitracker-modal-list" style="min-height: 100px;min-width: 200px;">
  2229. <div class="btn-group">
  2230. <input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-modal-search" placeholder="Search">
  2231. <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>
  2232. </div>
  2233. </div>
  2234. </div>
  2235. `).appendTo('#anitracker-modal-body');
  2236.  
  2237. $('.anitracker-modal-search').on('input', (e) => {
  2238. setTimeout(() => {
  2239. const query = $(e.target).val();
  2240. for (const entry of $('.anitracker-modal-list-entry')) {
  2241. if ($($(entry).find('a,span')[0]).text().toLowerCase().includes(query)) {
  2242. $(entry).show();
  2243. continue;
  2244. }
  2245. $(entry).hide();
  2246. }
  2247. }, 10);
  2248. });
  2249.  
  2250. function applyDeleteEvents() {
  2251. $('.anitracker-modal-list-entry button').on('click', (e) => {
  2252. const id = $(e.currentTarget).parent().attr('animeid');
  2253. toggleBookmark(id);
  2254.  
  2255. const data = getAnimeData();
  2256. if (data !== undefined && data.id === +id) {
  2257. $('.anitracker-bookmark-toggle .anitracker-title-icon-check').hide();
  2258. }
  2259.  
  2260. $(e.currentTarget).parent().remove();
  2261. });
  2262. }
  2263.  
  2264. // When clicking the reverse order button
  2265. $('.anitracker-reverse-order-button').on('click', (e) => {
  2266. const btn = $(e.target);
  2267. if (btn.attr('dir') === 'down') {
  2268. btn.attr('dir', 'up');
  2269. btn.addClass('anitracker-up');
  2270. }
  2271. else {
  2272. btn.attr('dir', 'down');
  2273. btn.removeClass('anitracker-up');
  2274. }
  2275.  
  2276. const entries = [];
  2277. for (const entry of $('.anitracker-modal-list-entry')) {
  2278. entries.push(entry.outerHTML);
  2279. }
  2280. entries.reverse();
  2281. $('.anitracker-modal-list-entry').remove();
  2282. for (const entry of entries) {
  2283. $(entry).appendTo($('.anitracker-modal-list'));
  2284. }
  2285. applyDeleteEvents();
  2286. });
  2287.  
  2288. [...storage.bookmarks].reverse().forEach(g => {
  2289. $(`
  2290. <div class="anitracker-modal-list-entry" animeid="${g.id}">
  2291. <a href="/a/${g.id}" target="_blank" title="${toHtmlCodes(g.name)}">
  2292. ${g.name}
  2293. </a><br>
  2294. <button class="btn btn-danger" title="Remove this bookmark">
  2295. <i class="fa fa-trash" aria-hidden="true"></i>
  2296. &nbsp;Remove
  2297. </button>
  2298. </div>`).appendTo('#anitracker-modal-body .anitracker-modal-list')
  2299. });
  2300. if (storage.bookmarks.length === 0) {
  2301. $(`<span style="display: block;">No bookmarks yet!</span>`).appendTo('#anitracker-modal-body .anitracker-modal-list');
  2302. }
  2303.  
  2304. applyDeleteEvents();
  2305. openModal();
  2306. $('#anitracker-modal-body')[0].scrollTop = 0;
  2307. });
  2308.  
  2309. function toggleBookmark(id, name=undefined) {
  2310. const storage = getStorage();
  2311. const found = storage.bookmarks.find(g => g.id === +id);
  2312.  
  2313. if (found !== undefined) {
  2314. const index = storage.bookmarks.indexOf(found);
  2315. storage.bookmarks.splice(index, 1);
  2316.  
  2317. saveData(storage);
  2318.  
  2319. return false;
  2320. }
  2321.  
  2322. if (name === undefined) return false;
  2323.  
  2324. storage.bookmarks.push({
  2325. id: +id,
  2326. name: name
  2327. });
  2328. saveData(storage);
  2329.  
  2330. return true;
  2331. }
  2332.  
  2333. function toggleNotifications(name, id = undefined) {
  2334. const storage = getStorage();
  2335. const found = (() => {
  2336. if (id !== undefined) return storage.notifications.anime.find(g => g.id === id);
  2337. else return storage.notifications.anime.find(g => g.name === name);
  2338. })();
  2339.  
  2340. if (found !== undefined) {
  2341. const index = storage.notifications.anime.indexOf(found);
  2342. storage.notifications.anime.splice(index, 1);
  2343.  
  2344. 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
  2345.  
  2346. saveData(storage);
  2347.  
  2348. return false;
  2349. }
  2350.  
  2351. const animeData = getAnimeData(name);
  2352.  
  2353. storage.notifications.anime.push({
  2354. name: name,
  2355. id: animeData.id
  2356. });
  2357. saveData(storage);
  2358.  
  2359. return true;
  2360. }
  2361.  
  2362. async function updateNotifications(animeName, storage = getStorage()) {
  2363. const nobj = storage.notifications.anime.find(g => g.name === animeName);
  2364. if (nobj === undefined) {
  2365. toggleNotifications(animeName);
  2366. return;
  2367. }
  2368. const data = await asyncGetAnimeData(animeName, nobj.id);
  2369. if (data === undefined) return -1;
  2370. const episodes = await asyncGetAllEpisodes(data.session, 'desc');
  2371. if (episodes === undefined) return 0;
  2372.  
  2373. return new Promise((resolve, reject) => {
  2374. if (episodes.length === 0) resolve(undefined);
  2375.  
  2376. nobj.latest_episode = episodes[0].created_at;
  2377.  
  2378. if (nobj.name !== data.title) {
  2379. for (const ep of storage.notifications.episodes) {
  2380. if (ep.animeName !== nobj.name) continue;
  2381. ep.animeName = data.title;
  2382. }
  2383. nobj.name = data.title;
  2384. }
  2385.  
  2386. const compareUpdateTime = nobj.updateFrom ?? storage.notifications.lastUpdated;
  2387. if (nobj.updateFrom !== undefined) delete nobj.updateFrom;
  2388.  
  2389. for (const ep of episodes) {
  2390. 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);
  2391. if (found !== undefined) {
  2392. found.session = ep.session;
  2393. if (found.animeId === undefined) found.animeId = nobj.id;
  2394.  
  2395. if (episodes.indexOf(ep) === episodes.length - 1) nobj.hasFirstEpisode = true;
  2396. continue;
  2397. }
  2398.  
  2399. if (new Date(ep.created_at + " UTC").getTime() < compareUpdateTime) {
  2400. continue;
  2401. }
  2402.  
  2403. storage.notifications.episodes.push({
  2404. animeName: nobj.name,
  2405. animeId: nobj.id,
  2406. session: ep.session,
  2407. episode: ep.episode,
  2408. time: ep.created_at,
  2409. watched: false
  2410. });
  2411. }
  2412.  
  2413. const length = storage.notifications.episodes.length;
  2414. if (length > 100) {
  2415. storage.notifications.episodes = storage.notifications.episodes.slice(length - 100);
  2416. }
  2417.  
  2418. saveData(storage);
  2419.  
  2420. resolve(data);
  2421. });
  2422. }
  2423.  
  2424. const paramArray = Array.from(new URLSearchParams(window.location.search));
  2425.  
  2426. const refArg01 = paramArray.find(a => a[0] === 'ref');
  2427. if (refArg01 !== undefined) {
  2428. const ref = refArg01[1];
  2429. if (ref === '404') {
  2430. alert('[AnimePahe Improvements]\n\nThe session was outdated, and has been refreshed. Please try that link again.');
  2431. }
  2432. else if (ref === 'customlink' && isEpisode() && initialStorage.autoDelete) {
  2433. const name = getAnimeName();
  2434. const num = getEpisodeNum();
  2435. if (initialStorage.linkList.find(e => e.animeName === name && e.type === 'episode' && e.episodeNum !== num)) { // If another episode is already stored
  2436. $(`
  2437. <span style="display:block;width:100%;text-align:center;" class="anitracker-from-share-warning">
  2438. The current episode data for this anime was not replaced due to coming from a share link.
  2439. <br>Refresh this page to replace it.
  2440. <br><span class="anitracker-text-button" tabindex="0">Dismiss</span>
  2441. </span>`).prependTo('.content-wrapper');
  2442.  
  2443. $('.anitracker-from-share-warning>span').on('click keydown', function(e) {
  2444. if (e.type === 'keydown' && e.key !== "Enter") return;
  2445. $(e.target).parent().remove();
  2446. });
  2447. }
  2448. }
  2449.  
  2450. window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
  2451. }
  2452.  
  2453. function getCurrentSeason() {
  2454. const month = new Date().getMonth();
  2455. return Math.trunc(month/3);
  2456. }
  2457.  
  2458. // Search/index page
  2459. if (/^\/anime\/?$/.test(window.location.pathname)) {
  2460. $(`
  2461. <div id="anitracker" class="anitracker-index" style="margin-bottom: 10px;">
  2462.  
  2463. <button class="btn btn-dark" id="anitracker-random-anime" title="Open a random anime from within the selected filters">
  2464. <i class="fa fa-random" aria-hidden="true"></i>
  2465. &nbsp;Random Anime
  2466. </button>
  2467.  
  2468. <div class="anitracker-items-box" id="anitracker-genre-list" dropdown="genre">
  2469. <button default="and" title="Toggle filter logic">and</button>
  2470. <div>
  2471. <div class="anitracker-items-box-search" contenteditable="" spellcheck="false"><span class="anitracker-text-input"></span></div>
  2472. <span class="placeholder">Genre</span>
  2473. </div>
  2474. </div>
  2475.  
  2476. <div class="anitracker-items-box" id="anitracker-theme-list" dropdown="theme">
  2477. <button default="and" title="Toggle filter logic">and</button>
  2478. <div>
  2479. <div class="anitracker-items-box-search" contenteditable="" spellcheck="false"><span class="anitracker-text-input"></span></div>
  2480. <span class="placeholder">Theme</span>
  2481. </div>
  2482. </div>
  2483.  
  2484. <div class="anitracker-items-box" id="anitracker-type-list" dropdown="type">
  2485. <div>
  2486. <div class="anitracker-items-box-search" contenteditable="" spellcheck="false"><span class="anitracker-text-input"></span></div>
  2487. <span class="placeholder">Type (or)</span>
  2488. </div>
  2489. </div>
  2490.  
  2491. <div class="anitracker-items-box" id="anitracker-demographic-list" dropdown="demographic">
  2492. <div>
  2493. <div class="anitracker-items-box-search" contenteditable="" spellcheck="false"><span class="anitracker-text-input"></span></div>
  2494. <span class="placeholder">Demographic (or)</span>
  2495. </div>
  2496. </div>
  2497.  
  2498. <div class="btn-group">
  2499. <button class="btn dropdown-toggle btn-dark" id="anitracker-status-button" data-bs-toggle="dropdown" data-toggle="dropdown" title="Choose status">All</button>
  2500. </div>
  2501.  
  2502. <div class="btn-group">
  2503. <button class="btn btn-dark" id="anitracker-time-search-button" title="Set season filter">
  2504. <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">
  2505. <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"/>
  2506. </svg>
  2507. </button>
  2508. </div>
  2509.  
  2510. </div>`).insertBefore('.index');
  2511.  
  2512. $('.anitracker-items-box-search').on('focus click', (e) => {
  2513. showDropdown(e.currentTarget);
  2514. });
  2515.  
  2516. function showDropdown(elem) {
  2517. $('.anitracker-dropdown-content').css('display', '');
  2518. const dropdown = $(`#anitracker-${$(elem).closest('.anitracker-items-box').attr('dropdown')}-dropdown`);
  2519. dropdown.show();
  2520. dropdown.css('position', 'absolute');
  2521. const pos = $(elem).closest('.anitracker-items-box-search').position();
  2522. dropdown.css('left', pos.left);
  2523. dropdown.css('top', pos.top + 40);
  2524. }
  2525.  
  2526. $('.anitracker-items-box-search').on('blur', (e) => {
  2527. setTimeout(() => {
  2528. const dropdown = $(`#anitracker-${$(e.target).parents().eq(1).attr('dropdown')}-dropdown`);
  2529. if (dropdown.is(':active') || dropdown.is(':focus')) return;
  2530. dropdown.hide();
  2531. }, 10);
  2532. });
  2533.  
  2534. $('.anitracker-items-box-search').on('keydown', (e) => {
  2535. setTimeout(() => {
  2536. const targ =$(e.target);
  2537.  
  2538. const type = targ.parents().eq(1).attr('dropdown');
  2539. const dropdown = $(`#anitracker-${type}-dropdown`);
  2540.  
  2541. for (const icon of targ.find('.anitracker-filter-icon')) {
  2542. (() => {
  2543. if ($(icon).text() === $(icon).data('name')) return;
  2544. const filter = $(icon).data('filter');
  2545. $(icon).remove();
  2546. for (const active of dropdown.find('.anitracker-active')) {
  2547. if ($(active).attr('ref') !== filter) continue;
  2548. removeFilter(filter, targ, $(active));
  2549. return;
  2550. }
  2551. removeFilter(filter, targ, undefined);
  2552. })();
  2553. }
  2554. if (dropdown.find('.anitracker-active').length > targ.find('.anitracker-filter-icon').length) {
  2555. const filters = [];
  2556. for (const icon of targ.find('.anitracker-filter-icon')) {
  2557. filters.push($(icon).data('filter'));
  2558. }
  2559.  
  2560. let removedFilter = false;
  2561. for (const active of dropdown.find('.anitracker-active')) {
  2562. if (filters.includes($(active).attr('ref'))) continue;
  2563. removedFilter = true;
  2564. removeFilter($(active).attr('ref'), targ, $(active), false);
  2565. }
  2566. if (removedFilter) refreshSearchPage(appliedFilters);
  2567. }
  2568. for (const filter of appliedFilters) { // Special case for non-default filters
  2569. (() => {
  2570. const parts = getFilterParts(filter);
  2571. if (parts.type !== type || filterValues[parts.type].includes(parts.value)) return;
  2572. for (const icon of targ.find('.anitracker-filter-icon')) {
  2573. if ($(icon).data('filter') === filter) return;
  2574. }
  2575.  
  2576. appliedFilters.splice(appliedFilters.indexOf(filter), 1);
  2577. refreshSearchPage(appliedFilters);
  2578. })();
  2579. }
  2580. targ.find('br').remove();
  2581.  
  2582. updateFilterBox(targ[0]);
  2583. }, 10);
  2584. });
  2585.  
  2586. function setIconEvent(elem) {
  2587. $(elem).on('click', (e) => {
  2588. const targ = $(e.target);
  2589. for (const btn of $(`#anitracker-${targ.closest('.anitracker-items-box').attr('dropdown')}-dropdown button`)) {
  2590. if ($(btn).attr('ref') !== targ.data('filter')) continue;
  2591. removeFilter(targ.data('filter'), targ.parent(), btn);
  2592. return;
  2593. }
  2594. removeFilter(targ.data('filter'), targ.parent(), undefined);
  2595. });
  2596. }
  2597.  
  2598. function updateFilterBox(elem) {
  2599. const targ = $(elem);
  2600.  
  2601. for (const icon of targ.find('.anitracker-filter-icon')) {
  2602. if (appliedFilters.includes($(icon).data('filter'))) continue;
  2603. $(icon).remove();
  2604. }
  2605.  
  2606. if (appliedFilters.length === 0) {
  2607. for (const input of targ.find('.anitracker-text-input')) {
  2608. if ($(input).text().trim() !== '') continue;
  2609. $(input).text('');
  2610. }
  2611. }
  2612.  
  2613. const text = getFilterBoxText(targ[0]).trim();
  2614.  
  2615. const dropdownBtns = $(`#anitracker-${targ.parents().eq(1).attr('dropdown')}-dropdown button`);
  2616. dropdownBtns.show();
  2617. if (text !== '') {
  2618. for (const btn of dropdownBtns) {
  2619. if ($(btn).text().toLowerCase().includes(text.toLowerCase())) continue;
  2620. $(btn).hide();
  2621. }
  2622. }
  2623.  
  2624. if (targ.text().trim() === '') {
  2625. targ.text('');
  2626. targ.parent().find('.placeholder').show();
  2627. return;
  2628. }
  2629. targ.parent().find('.placeholder').hide();
  2630. }
  2631.  
  2632. function getFilterBoxText(elem) {
  2633. const basicText = $(elem).contents().filter(function(){return this.nodeType == Node.TEXT_NODE;})[0]
  2634. const spanText = $($(elem).find('.anitracker-text-input')[$(elem).find('.anitracker-text-input').length-1]).text() + '';
  2635. if (basicText === undefined) return spanText;
  2636. return (basicText.nodeValue + spanText).trim();
  2637. }
  2638.  
  2639. $('.anitracker-items-box>button').on('click', (e) => {
  2640. const targ = $(e.target);
  2641. const newRule = targ.text() === 'and' ? 'or' : 'and';
  2642. const type = targ.parent().attr('dropdown');
  2643. filterRules[type] = newRule;
  2644. targ.text(newRule);
  2645. const filterBox = targ.parent().find('.anitracker-items-box-search');
  2646. filterBox.focus();
  2647. const filterList = appliedFilters.filter(a => a.startsWith(type + '/'));
  2648. if (newRule === 'and' && filterList.length > 1 && filterList.find(a => a.startsWith(type + '/none')) !== undefined) {
  2649. for (const btn of $(`#anitracker-${type}-dropdown button`)) {
  2650. if ($(btn).attr('ref') !== type + '/none' ) continue;
  2651. removeFilter(type + '/none', filterBox, btn, false);
  2652. break;
  2653. }
  2654. }
  2655. if (filterList.length > 0) refreshSearchPage(appliedFilters);
  2656. });
  2657.  
  2658. const animeList = getAnimeList();
  2659.  
  2660. $(`
  2661. <span style="display: block;margin-bottom: 10px;font-size: 1.2em;color:#ddd;" id="anitracker-filter-result-count">Filter results: <span>${animeList.length}</span></span>
  2662. `).insertAfter('#anitracker');
  2663.  
  2664. $('#anitracker-random-anime').on('click', function() {
  2665. const storage = getStorage();
  2666. storage.cache = filterSearchCache;
  2667. saveData(storage);
  2668.  
  2669. const params = getParams(appliedFilters, $('.anitracker-items-box>button'));
  2670.  
  2671. if ($('#anitracker-anime-list-search').length > 0 && $('#anitracker-anime-list-search').val() !== '') {
  2672. $.getScript('https://cdn.jsdelivr.net/npm/fuse.js@7.0.0', function() {
  2673. const query = $('#anitracker-anime-list-search').val();
  2674.  
  2675. getRandomAnime(searchList(Fuse, animeList, query), (params === '' ? '?anitracker-random=1' : params + '&anitracker-random=1') + '&search=' + encodeURIComponent(query));
  2676. });
  2677. }
  2678. else {
  2679. getRandomAnime(animeList, params === '' ? '?anitracker-random=1' : params + '&anitracker-random=1');
  2680. }
  2681. });
  2682.  
  2683. function getDropdownButtons(filters, type) {
  2684. return filters.sort((a,b) => a.name > b.name ? 1 : -1).map(g => $(`<button ref="${type}/${g.value}">${g.name}</button>`));
  2685. }
  2686.  
  2687. $(`<div id="anitracker-genre-dropdown" dropdown="genre" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-genre-list');
  2688. getDropdownButtons(filterValues.genre, 'genre').forEach(g => { g.appendTo('#anitracker-genre-dropdown') });
  2689. $(`<button ref="genre/none">(None)</button>`).appendTo('#anitracker-genre-dropdown');
  2690.  
  2691. $(`<div id="anitracker-theme-dropdown" dropdown="theme" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-theme-list');
  2692. getDropdownButtons(filterValues.theme, 'theme').forEach(g => { g.appendTo('#anitracker-theme-dropdown') });
  2693. $(`<button ref="theme/none">(None)</button>`).appendTo('#anitracker-theme-dropdown');
  2694.  
  2695. $(`<div id="anitracker-type-dropdown" dropdown="type" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-type-list');
  2696. getDropdownButtons(filterValues.type, 'type').forEach(g => { g.appendTo('#anitracker-type-dropdown') });
  2697. $(`<button ref="type/none">(None)</button>`).appendTo('#anitracker-type-dropdown');
  2698.  
  2699. $(`<div id="anitracker-demographic-dropdown" dropdown="demographic" class="dropdown-menu anitracker-dropdown-content anitracker-items-dropdown">`).insertAfter('#anitracker-demographic-list');
  2700. getDropdownButtons(filterValues.demographic, 'demographic').forEach(g => { g.appendTo('#anitracker-demographic-dropdown') });
  2701. $(`<button ref="demographic/none">(None)</button>`).appendTo('#anitracker-demographic-dropdown');
  2702.  
  2703. $(`<div id="anitracker-status-dropdown" dropdown="status" class="dropdown-menu anitracker-dropdown-content">`).insertAfter('#anitracker-status-button');
  2704. ['all','airing','completed'].forEach(g => { $(`<button ref="${g}">${g[0].toUpperCase() + g.slice(1)}</button>`).appendTo('#anitracker-status-dropdown') });
  2705. $(`<button ref="none">(No status)</button>`).appendTo('#anitracker-status-dropdown');
  2706.  
  2707. const timeframeSettings = {
  2708. enabled: false
  2709. };
  2710.  
  2711. $('#anitracker-time-search-button').on('click', () => {
  2712. $('#anitracker-modal-body').empty();
  2713.  
  2714. $(`
  2715. <h5>Time interval</h5>
  2716. <div class="custom-control custom-switch">
  2717. <input type="checkbox" class="custom-control-input" id="anitracker-settings-enable-switch">
  2718. <label class="custom-control-label" for="anitracker-settings-enable-switch">Enable</label>
  2719. </div>
  2720. <br>
  2721. <div class="anitracker-season-group" id="anitracker-season-from">
  2722. <span>From:</span>
  2723. <div class="btn-group">
  2724. <input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-year-input" disabled placeholder="Year" type="number">
  2725. </div>
  2726. <div class="btn-group">
  2727. <button class="btn dropdown-toggle btn-secondary anitracker-season-dropdown-button" disabled data-bs-toggle="dropdown" data-toggle="dropdown" data-value="Spring">Spring</button>
  2728. </div>
  2729. <div class="btn-group">
  2730. <button class="btn btn-secondary" id="anitracker-season-copy-to-lower" style="color:white;margin-left:14px;" title="Copy the 'from' season to the 'to' season">
  2731. <i class="fa fa-arrow-circle-down" aria-hidden="true"></i>
  2732. </button>
  2733. </div>
  2734. </div>
  2735. <div class="anitracker-season-group" id="anitracker-season-to">
  2736. <span>To:</span>
  2737. <div class="btn-group">
  2738. <input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-year-input" disabled placeholder="Year" type="number">
  2739. </div>
  2740. <div class="btn-group">
  2741. <button class="btn dropdown-toggle btn-secondary anitracker-season-dropdown-button" disabled data-bs-toggle="dropdown" data-toggle="dropdown" data-value="Spring">Spring</button>
  2742. </div>
  2743. </div>
  2744. <br>
  2745. <div>
  2746. <div class="btn-group">
  2747. <button class="btn btn-primary" id="anitracker-modal-confirm-button"><i class="fa fa-check" aria-hidden="true"></i>&nbsp;Done</button>
  2748. </div>
  2749. </div>`).appendTo('#anitracker-modal-body');
  2750.  
  2751. $('.anitracker-year-input').val(new Date().getFullYear());
  2752.  
  2753. $('#anitracker-settings-enable-switch').on('change', () => {
  2754. updateDisabled($('#anitracker-settings-enable-switch').is(':checked'));
  2755. });
  2756. $('#anitracker-settings-enable-switch').prop('checked', timeframeSettings.enabled);
  2757. updateDisabled(timeframeSettings.enabled);
  2758.  
  2759. function updateDisabled(enabled) {
  2760. $('.anitracker-season-group').find('input,button').prop('disabled', !enabled);
  2761. }
  2762.  
  2763. $('#anitracker-season-copy-to-lower').on('click', () => {
  2764. const seasonName = $('#anitracker-season-from .anitracker-season-dropdown-button').data('value');
  2765. $('#anitracker-season-to .anitracker-year-input').val($('#anitracker-season-from .anitracker-year-input').val());
  2766. $('#anitracker-season-to .anitracker-season-dropdown-button').data('value', seasonName);
  2767. $('#anitracker-season-to .anitracker-season-dropdown-button').text(seasonName);
  2768. });
  2769.  
  2770. $(`<div class="dropdown-menu anitracker-dropdown-content anitracker-season-dropdown">`).insertAfter('.anitracker-season-dropdown-button');
  2771. ['Winter','Spring','Summer','Fall'].forEach(g => { $(`<button ref="${g.toLowerCase()}">${g}</button>`).appendTo('.anitracker-season-dropdown') });
  2772.  
  2773. $('.anitracker-season-dropdown button').on('click', (e) => {
  2774. const pressed = $(e.target)
  2775. const btn = pressed.parents().eq(1).find('.anitracker-season-dropdown-button');
  2776. btn.data('value', pressed.text());
  2777. btn.text(pressed.text());
  2778. });
  2779.  
  2780. const currentSeason = getCurrentSeason();
  2781. if (timeframeSettings.from) {
  2782. $('#anitracker-season-from .anitracker-year-input').val(timeframeSettings.from.year.toString());
  2783. $('#anitracker-season-from .anitracker-season-dropdown button')[timeframeSettings.from.season].click();
  2784. }
  2785. else {
  2786. $('#anitracker-season-from .anitracker-season-dropdown button')[currentSeason].click();
  2787. }
  2788. if (timeframeSettings.to) {
  2789. $('#anitracker-season-to .anitracker-year-input').val(timeframeSettings.to.year.toString());
  2790. $('#anitracker-season-to .anitracker-season-dropdown button')[timeframeSettings.to.season].click();
  2791. }
  2792. else {
  2793. $('#anitracker-season-to .anitracker-season-dropdown button')[currentSeason].click();
  2794. }
  2795.  
  2796. $('#anitracker-modal-confirm-button').on('click', () => {
  2797. const from = {
  2798. year: +$('#anitracker-season-from .anitracker-year-input').val(),
  2799. season: getSeasonValue($('#anitracker-season-from').find('.anitracker-season-dropdown-button').data('value'))
  2800. }
  2801. const to = {
  2802. year: +$('#anitracker-season-to .anitracker-year-input').val(),
  2803. season: getSeasonValue($('#anitracker-season-to').find('.anitracker-season-dropdown-button').data('value'))
  2804. }
  2805. if ($('#anitracker-settings-enable-switch').is(':checked')) {
  2806. for (const input of $('.anitracker-year-input')) {
  2807. if (/^\d{4}$/.test($(input).val())) continue;
  2808. alert('[AnimePahe Improvements]\n\nYear values must both be 4 numbers.');
  2809. return;
  2810. }
  2811. if (to.year < from.year || (to.year === from.year && to.season < from.season)) {
  2812. alert('[AnimePahe Improvements]\n\nSeason times must be from oldest to newest.' + (to.season === 0 ? '\n(Winter comes before spring)' : ''));
  2813. return;
  2814. }
  2815. if (to.year - from.year > 100) {
  2816. alert('[AnimePahe Improvements]\n\nYear interval cannot be more than 100 years.');
  2817. return;
  2818. }
  2819. removeSeasonsFromFilters();
  2820. appliedFilters.push(`season/${getSeasonName(from.season)}-${from.year.toString()}..${getSeasonName(to.season)}-${to.year.toString()}`);
  2821. $('#anitracker-time-search-button').addClass('anitracker-active');
  2822. }
  2823. else {
  2824. removeSeasonsFromFilters();
  2825. $('#anitracker-time-search-button').removeClass('anitracker-active');
  2826. }
  2827. timeframeSettings.enabled = $('#anitracker-settings-enable-switch').is(':checked');
  2828. timeframeSettings.from = from;
  2829. timeframeSettings.to = to;
  2830. closeModal();
  2831. refreshSearchPage(appliedFilters, true);
  2832. });
  2833.  
  2834. openModal();
  2835. });
  2836.  
  2837. function removeSeasonsFromFilters() {
  2838. const newFilters = [];
  2839. for (const filter of appliedFilters) {
  2840. if (filter.startsWith('season/')) continue;
  2841. newFilters.push(filter);
  2842. }
  2843. appliedFilters.length = 0;
  2844. appliedFilters.push(...newFilters);
  2845. }
  2846.  
  2847. const appliedFilters = [];
  2848.  
  2849. $('.anitracker-items-dropdown').on('click', (e) => {
  2850. const filterSearchBox = $(`#anitracker-${/^anitracker-([^\-]+)-dropdown$/.exec($(e.target).closest('.anitracker-dropdown-content').attr('id'))[1]}-list .anitracker-items-box-search`);
  2851. filterSearchBox.focus();
  2852.  
  2853. if (!$(e.target).is('button')) return;
  2854.  
  2855. const filter = $(e.target).attr('ref');
  2856. if (appliedFilters.includes(filter)) {
  2857. removeFilter(filter, filterSearchBox, e.target);
  2858. }
  2859. else {
  2860. addFilter(filter, filterSearchBox, e.target);
  2861. }
  2862. });
  2863.  
  2864. $('#anitracker-status-dropdown').on('click', (e) => {
  2865. if (!$(e.target).is('button')) return;
  2866.  
  2867. const filter = $(e.target).attr('ref');
  2868. addStatusFilter(filter);
  2869. refreshSearchPage(appliedFilters);
  2870. });
  2871.  
  2872. function addStatusFilter(filter) {
  2873. if (appliedFilters.includes(filter)) return;
  2874. for (const btn of $('#anitracker-status-dropdown button')) {
  2875. if ($(btn).attr('ref') !== filter) continue;
  2876. $('#anitracker-status-button').text($(btn).text());
  2877. }
  2878.  
  2879. if (filter !== 'all') $('#anitracker-status-button').addClass('anitracker-active');
  2880. else $('#anitracker-status-button').removeClass('anitracker-active');
  2881.  
  2882. for (const filter2 of appliedFilters) {
  2883. if (filter2.includes('/')) continue;
  2884. appliedFilters.splice(appliedFilters.indexOf(filter2), 1);
  2885. }
  2886. if (filter !== 'all') appliedFilters.push(filter);
  2887. }
  2888.  
  2889. function addFilter(name, filterBox, filterButton, refreshPage = true) {
  2890. const filterType = getFilterParts(name).type;
  2891. if (filterType !== '' && filterRules[filterType] === 'and') {
  2892. if (name.endsWith('/none')) {
  2893. for (const filter of appliedFilters.filter(a => a.startsWith(filterType))) {
  2894. if (filter.endsWith('/none')) continue;
  2895.  
  2896. removeFilter(filter, filterBox, (() => {
  2897. for (const btn of $(filterButton).parent().find('button')) {
  2898. if ($(btn).attr('ref') !== filter) continue;
  2899. return btn;
  2900. }
  2901. })(), false);
  2902. }
  2903. }
  2904. else if (appliedFilters.includes(filterType + '/none')) {
  2905. removeFilter(filterType + '/none', filterBox, (() => {
  2906. for (const btn of $(filterButton).parent().find('button')) {
  2907. if ($(btn).attr('ref') !== filterType + '/none') continue;
  2908. return btn;
  2909. }
  2910. })(), false);
  2911. }
  2912. }
  2913.  
  2914. $(filterBox).find('.anitracker-text-input').text('');
  2915. const basicText = $(filterBox).contents().filter(function(){return this.nodeType == Node.TEXT_NODE;})[0];
  2916. if (basicText !== undefined) basicText.nodeValue = '';
  2917. addFilterIcon($(filterBox)[0], name, $(filterButton).text());
  2918. $(filterButton).addClass('anitracker-active');
  2919. appliedFilters.push(name);
  2920.  
  2921. if (refreshPage) refreshSearchPage(appliedFilters);
  2922. updateFilterBox(filterBox);
  2923. }
  2924.  
  2925. function removeFilter(name, filterBox, filterButton, refreshPage = true) {
  2926. $(filterBox).find('.anitracker-text-input').text('');
  2927. const basicText = $(filterBox).contents().filter(function(){return this.nodeType == Node.TEXT_NODE;})[0];
  2928. if (basicText !== undefined) basicText.nodeValue = '';
  2929.  
  2930. removeFilterIcon($(filterBox)[0], name);
  2931. $(filterButton).removeClass('anitracker-active');
  2932. appliedFilters.splice(appliedFilters.indexOf(name), 1);
  2933.  
  2934. if (refreshPage) refreshSearchPage(appliedFilters);
  2935. updateFilterBox(filterBox);
  2936. }
  2937.  
  2938. function addFilterIcon(elem, filter, nameInput) {
  2939. const name = nameInput || getFilterParts(filter).value;
  2940. setIconEvent($(`
  2941. <span class="anitracker-filter-icon" data-name="${name}" data-filter="${filter}">${name}</span><span class="anitracker-text-input">&nbsp;</span>
  2942. `).after(' ').appendTo(elem));
  2943. }
  2944.  
  2945. function removeFilterIcon(elem, name) {
  2946. for (const f of $(elem).find('.anitracker-filter-icon')) {
  2947. if ($(f).text() === name) $(f).remove();
  2948. }
  2949. }
  2950.  
  2951. const searchQueue = [];
  2952.  
  2953. function refreshSearchPage(filtersInput, screenSpinner = false, fromQueue = false) {
  2954. const filters = JSON.parse(JSON.stringify(filtersInput));
  2955. if (!fromQueue) {
  2956. if (screenSpinner) {
  2957. $(`
  2958. <div style="width:100%;height:100%;background-color:rgba(0, 0, 0, 0.9);position:fixed;z-index:999;display:flex;justify-content:center;align-items:center;" class="anitracker-filter-spinner">
  2959. <div class="spinner-border" role="status" style="color:#d5015b;width:5rem;height:5rem;">
  2960. <span class="sr-only">Loading...</span>
  2961. </div>
  2962. <span style="position: absolute;font-weight: bold;">0%</span>
  2963. </div>`).prependTo(document.body);
  2964. }
  2965. else {
  2966. $(`
  2967. <div style="display: inline-flex;margin-left: 10px;justify-content: center;align-items: center;vertical-align: bottom;" class="anitracker-filter-spinner">
  2968. <div class="spinner-border" role="status" style="color:#d5015b;">
  2969. <span class="sr-only">Loading...</span>
  2970. </div>
  2971. <span style="position: absolute;font-size: .5em;font-weight: bold;">0%</span>
  2972. </div>`).appendTo('.page-index h1');
  2973. }
  2974. searchQueue.push(filters);
  2975. if (searchQueue.length > 1) return;
  2976. }
  2977.  
  2978.  
  2979. if (filters.length === 0) {
  2980. updateFilterResults([], true).then(() => {
  2981. animeList.length = 0;
  2982. animeList.push(...getAnimeList());
  2983. $('#anitracker-filter-result-count span').text(animeList.length.toString());
  2984. $($('.anitracker-filter-spinner')[0]).remove();
  2985. searchQueue.shift();
  2986. if (searchQueue.length > 0) {
  2987. refreshSearchPage(searchQueue[0], screenSpinner, true);
  2988. return;
  2989. }
  2990.  
  2991. if ($('#anitracker-anime-list-search').val() === '') return;
  2992. $('#anitracker-anime-list-search').trigger('anitracker:search');
  2993. });
  2994. return;
  2995. }
  2996.  
  2997. let filterTotal = 0;
  2998. for (const filter of filters) {
  2999. const parts = getFilterParts(filter);
  3000. if (noneFilterRegex.test(filter)) {
  3001. filterTotal += filterValues[parts.type].length;
  3002. continue;
  3003. }
  3004. if (seasonFilterRegex.test(filter)) {
  3005. const range = parts.value.split('..');
  3006. filterTotal += getSeasonTimeframe({
  3007. year: +range[0].split('-')[1],
  3008. season: getSeasonValue(range[0].split('-')[0])
  3009. },
  3010. {
  3011. year: +range[1].split('-')[1],
  3012. season: getSeasonValue(range[1].split('-')[0])
  3013. }).length;
  3014. continue;
  3015. }
  3016. filterTotal++;
  3017. }
  3018.  
  3019. getFilteredList(filters, filterTotal).then((finalList) => {
  3020. if (finalList === undefined) {
  3021. alert('[AnimePahe Improvements]\n\nSearch filter failed.');
  3022.  
  3023. $($('.anitracker-filter-spinner')[0]).remove();
  3024. searchQueue.length = 0;
  3025. refreshSearchPage([]);
  3026. return;
  3027. }
  3028. finalList.sort((a,b) => a.name > b.name ? 1 : -1);
  3029.  
  3030. updateFilterResults(finalList).then(() => {
  3031. animeList.length = 0;
  3032. animeList.push(...finalList);
  3033.  
  3034. $($('.anitracker-filter-spinner')[0]).remove();
  3035.  
  3036. updateParams(appliedFilters, $('.anitracker-items-box>button'));
  3037.  
  3038. searchQueue.shift();
  3039. if (searchQueue.length > 0) {
  3040. refreshSearchPage(searchQueue[0], screenSpinner, true);
  3041. return;
  3042. }
  3043.  
  3044. if ($('#anitracker-anime-list-search').val() === '') return;
  3045. $('#anitracker-anime-list-search').trigger('anitracker:search');
  3046. });
  3047. });
  3048. }
  3049.  
  3050. function updateFilterResults(list, noFilters = false) {
  3051. return new Promise((resolve, reject) => {
  3052. $('.anitracker-filter-result').remove();
  3053. $('#anitracker-filter-results').remove();
  3054. $('.nav-item').show();
  3055.  
  3056. if (noFilters) {
  3057. $('.index>').show();
  3058. $('.index>>>>div').show();
  3059.  
  3060. updateParams(appliedFilters);
  3061.  
  3062. resolve();
  3063. return;
  3064. }
  3065.  
  3066. $('#anitracker-filter-result-count span').text(list.length.toString());
  3067.  
  3068. $('.index>>>>div').hide();
  3069.  
  3070. if (list.length >= 100) {
  3071. $('.index>').show();
  3072. list.forEach(anime => {
  3073. const elem = $(`
  3074. <div class="anitracker-filter-result col-12 col-md-6">
  3075. ${anime.html}
  3076. </div>`);
  3077.  
  3078. const matchLetter = (() => {
  3079. if (/^[A-Za-z]/.test(anime.name)) {
  3080. return anime.name[0].toUpperCase();
  3081. }
  3082. else {
  3083. return 'hash'
  3084. }
  3085. })();
  3086.  
  3087.  
  3088. for (const tab of $('.tab-content').children()) {
  3089. if (tab.id !== matchLetter) continue;
  3090.  
  3091. elem.appendTo($(tab).children()[0]);
  3092. }
  3093. });
  3094. for (const tab of $('.tab-content').children()) {
  3095. if ($(tab).find('.anitracker-filter-result').length > 0) continue;
  3096.  
  3097. const tabId = $(tab).attr('id');
  3098. for (const navLink of $('.nav-link')) {
  3099. if (($(navLink).attr('role') !== 'tab' || $(navLink).text() !== tabId) && !($(navLink).text() === '#' && tabId === 'hash')) continue;
  3100. $(navLink).parent().hide();
  3101. }
  3102. }
  3103. if ($('.nav-link.active').parent().css('display') === 'none') {
  3104. let visibleTabs = 0;
  3105. for (const navLink of $('.nav-link')) {
  3106. if ($(navLink).parent().css('display') === 'none' || $(navLink).text().length > 1) continue;
  3107. visibleTabs++;
  3108. }
  3109. for (const navLink of $('.nav-link')) {
  3110. if ($(navLink).parent().css('display') === 'none' || $(navLink).text().length > 1) continue;
  3111. if ($(navLink).text() === "#" && visibleTabs > 1) continue;
  3112. $(navLink).click();
  3113. break;
  3114. }
  3115. }
  3116. }
  3117. else {
  3118. $('.index>').hide();
  3119. $(`<div class="row" id="anitracker-filter-results"></div>`).prependTo('.index');
  3120.  
  3121. let matches = '';
  3122.  
  3123. list.forEach(anime => {
  3124. matches += `
  3125. <div class="col-12 col-md-6">
  3126. ${anime.html}
  3127. </div>`;
  3128. });
  3129.  
  3130. if (list.length === 0) matches = `<div class="col-12 col-md-6">No results found.</div>`;
  3131.  
  3132. $(matches).appendTo('#anitracker-filter-results');
  3133. }
  3134.  
  3135. resolve();
  3136. });
  3137. }
  3138.  
  3139. function updateParams(filters, ruleButtons = []) {
  3140. window.history.replaceState({}, document.title, "/anime" + getParams(filters, ruleButtons));
  3141. }
  3142.  
  3143. function getParams(filters, ruleButtons = []) {
  3144. const filterArgs = textFromFilterList(filters);
  3145. let params = (filterArgs.length > 0 ? ('?' + filterArgs) : '');
  3146. if (ruleButtons.length > 0) {
  3147. for (const btn of ruleButtons) {
  3148. if ($(btn).text() === $(btn).attr('default')) continue;
  3149. params += '&' + $(btn).parent().attr('dropdown') + '-rule=' + $(btn).text();
  3150. }
  3151. }
  3152. return params;
  3153. }
  3154.  
  3155. $.getScript('https://cdn.jsdelivr.net/npm/fuse.js@7.0.0', function() {
  3156. $(`
  3157. <div class="btn-group">
  3158. <input id="anitracker-anime-list-search" autocomplete="off" class="form-control anitracker-text-input-bar" style="width: 150px;" placeholder="Search">
  3159. </div>`).appendTo('#anitracker');
  3160.  
  3161. let typingTimer;
  3162.  
  3163. $('#anitracker-anime-list-search').on('anitracker:search', function() {
  3164. animeListSearch();
  3165. });
  3166.  
  3167. $('#anitracker-anime-list-search').on('keyup', function() {
  3168. clearTimeout(typingTimer);
  3169. typingTimer = setTimeout(animeListSearch, 150);
  3170. });
  3171.  
  3172. $('#anitracker-anime-list-search').on('keydown', function() {
  3173. clearTimeout(typingTimer);
  3174. });
  3175.  
  3176. function animeListSearch() {
  3177. $('#anitracker-search-results').remove();
  3178. const value = $('#anitracker-anime-list-search').val();
  3179. if (value === '') {
  3180. $('.index>').show();
  3181. if (animeList.length < 100) $('.scrollable-ul').hide();
  3182. const newSearchParams = new URLSearchParams(window.location.search);
  3183. newSearchParams.delete('search');
  3184. window.history.replaceState({}, document.title, "/anime" + (Array.from(newSearchParams.entries()).length > 0 ? ('?' + newSearchParams.toString()) : ''));
  3185. }
  3186. else {
  3187. $('.index>').hide();
  3188.  
  3189. const matches = searchList(Fuse, animeList, value);
  3190.  
  3191. $(`<div class="row" id="anitracker-search-results"></div>`).prependTo('.index');
  3192.  
  3193. let elements = '';
  3194.  
  3195. matches.forEach(match => {
  3196. elements += `
  3197. <div class="col-12 col-md-6">
  3198. ${match.html}
  3199. </div>`;
  3200. });
  3201.  
  3202. if (matches.length === 0) elements = `<div class="col-12 col-md-6">No results found.</div>`;
  3203.  
  3204. $(elements).appendTo('#anitracker-search-results');
  3205. const newSearchParams = new URLSearchParams(window.location.search);
  3206. newSearchParams.set('search', value);
  3207. window.history.replaceState({}, document.title, "/anime?" + newSearchParams.toString());
  3208. }
  3209. }
  3210.  
  3211. const searchParams = new URLSearchParams(window.location.search);
  3212. if (searchParams.has('search')) {
  3213. $('#anitracker-anime-list-search').val(searchParams.get('search'));
  3214. animeListSearch();
  3215. }
  3216. }).fail(() => {
  3217. console.error("[AnimePahe Improvements] Fuse.js failed to load");
  3218. });
  3219.  
  3220. const urlFilters = filterListFromParams(new URLSearchParams(window.location.search));
  3221. for (const filter of urlFilters) {
  3222. const parts = getFilterParts(filter);
  3223. const type = parts.type;
  3224. if (type === '') {
  3225. addStatusFilter(filter);
  3226. continue;
  3227. }
  3228.  
  3229. const searchBox = $(`#anitracker-${type}-list .anitracker-items-box-search`);
  3230. const dropdown = Array.from($(`#anitracker-${type}-dropdown`).children()).find(a=> $(a).attr('ref') === filter);
  3231.  
  3232. if (type.endsWith('-rule')) {
  3233. for (const btn of $('.anitracker-items-box>button')) {
  3234. const type2 = $(btn).parent().attr('dropdown');
  3235. if (type2 !== type.split('-')[0]) continue;
  3236. $(btn).text(parts.value);
  3237. }
  3238. continue;
  3239. }
  3240.  
  3241. if (type === 'season') {
  3242. if (!seasonFilterRegex.test(filter)) continue;
  3243. appliedFilters.push(filter);
  3244. $('#anitracker-time-search-button').addClass('anitracker-active');
  3245. const range = parts.value.split('..');
  3246. timeframeSettings.enabled = true;
  3247. timeframeSettings.from = {
  3248. year: +range[0].split('-')[1],
  3249. season: getSeasonValue(range[0].split('-')[0])
  3250. };
  3251. timeframeSettings.to = {
  3252. year: +range[1].split('-')[1],
  3253. season: getSeasonValue(range[1].split('-')[0])
  3254. };
  3255. continue;
  3256. }
  3257. if (searchBox.length === 0) {
  3258. appliedFilters.push(filter);
  3259. continue;
  3260. }
  3261.  
  3262. addFilter(filter, searchBox, dropdown, false);
  3263. continue;
  3264. }
  3265. if (urlFilters.length > 0) refreshSearchPage(appliedFilters, true);
  3266. return;
  3267. }
  3268.  
  3269. function filterListFromParams(params, allowRules = true) {
  3270. const filters = [];
  3271. for (const [key, values] of params.entries()) {
  3272. const key2 = (key === 'other' ? '' : key);
  3273. if (!filterRules[key2] && !key.endsWith('-rule')) continue;
  3274. if (key.endsWith('-rule')) {
  3275. filterRules[key.split('-')[0]] = values === 'and' ? 'and' : 'or';
  3276. if (!allowRules) continue;
  3277. }
  3278. decodeURIComponent(values).split(',').forEach(value => {
  3279. filters.push((key2 === '' ? '' : key2 + '/') + value);
  3280. });
  3281. }
  3282. return filters;
  3283. }
  3284.  
  3285. function textFromFilterList(filters) {
  3286. const filterTypes = {};
  3287. filters.forEach(filter => {
  3288. const parts = getFilterParts(filter);
  3289. let key = (() => {
  3290. if (parts.type === '') return 'other';
  3291. return parts.type;
  3292. })();
  3293.  
  3294. if (filterTypes[key] === undefined) filterTypes[key] = [];
  3295. filterTypes[key].push(parts.value);
  3296. });
  3297. const finishedList = [];
  3298. for (const [key, values] of Object.entries(filterTypes)) {
  3299. finishedList.push(key + '=' + encodeURIComponent(values.join(',')));
  3300. }
  3301. return finishedList.join('&');
  3302. }
  3303.  
  3304. function getAnimeList(page = $(document)) {
  3305. const animeList = [];
  3306.  
  3307. for (const anime of page.find('.col-12')) {
  3308. if (anime.children[0] === undefined || $(anime).hasClass('anitracker-filter-result') || $(anime).parent().attr('id') !== undefined) continue;
  3309. animeList.push({
  3310. name: $(anime.children[0]).text(),
  3311. link: anime.children[0].href,
  3312. html: $(anime).html()
  3313. });
  3314. }
  3315.  
  3316. return animeList;
  3317. }
  3318.  
  3319. function randint(min, max) { // min and max included
  3320. return Math.floor(Math.random() * (max - min + 1) + min);
  3321. }
  3322.  
  3323. function isEpisode(url = window.location.toString()) {
  3324. return url.includes('/play/');
  3325. }
  3326.  
  3327. function isAnime(url = window.location.pathname) {
  3328. return /^\/anime\/[\d\w\-]+$/.test(url);
  3329. }
  3330.  
  3331. function download(filename, text) {
  3332. var element = document.createElement('a');
  3333. element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
  3334. element.setAttribute('download', filename);
  3335.  
  3336. element.style.display = 'none';
  3337. document.body.appendChild(element);
  3338.  
  3339. element.click();
  3340.  
  3341. document.body.removeChild(element);
  3342. }
  3343.  
  3344. function deleteEpisodesFromTracker(exclude, nameInput, id = undefined) {
  3345. const storage = getStorage();
  3346. const animeName = nameInput || getAnimeName();
  3347. const linkData = getStoredLinkData(storage);
  3348.  
  3349. storage.linkList = (() => {
  3350. if (id !== undefined) {
  3351. const found = storage.linkList.filter(g => g.type === 'episode' && g.animeId === id && g.episodeNum !== exclude);
  3352. if (found.length > 0) return storage.linkList.filter(g => !(g.type === 'episode' && g.animeId === id && g.episodeNum !== exclude));
  3353. }
  3354.  
  3355. return storage.linkList.filter(g => !(g.type === 'episode' && g.animeName === animeName && g.episodeNum !== exclude));
  3356. })();
  3357.  
  3358. storage.videoTimes = (() => {
  3359. if (id !== undefined) {
  3360. const found = storage.videoTimes.filter(g => g.animeId === id && g.episodeNum !== exclude);
  3361. if (found.length > 0) return storage.videoTimes.filter(g => !(g.animeId === id && g.episodeNum !== exclude));
  3362. }
  3363.  
  3364. return storage.videoTimes.filter(g => !(g.episodeNum !== exclude && stringSimilarity(g.animeName, animeName) > 0.81));
  3365. })();
  3366.  
  3367. saveData(storage);
  3368. }
  3369.  
  3370. function deleteEpisodeFromTracker(animeName, episodeNum, animeId = undefined) {
  3371. const storage = getStorage();
  3372.  
  3373. storage.linkList = (() => {
  3374. if (animeId !== undefined) {
  3375. const found = storage.linkList.find(g => g.type === 'episode' && g.animeId === animeId && g.episodeNum === episodeNum);
  3376. if (found !== undefined) return storage.linkList.filter(g => !(g.type === 'episode' && g.animeId === animeId && g.episodeNum === episodeNum));
  3377. }
  3378.  
  3379. return storage.linkList.filter(g => !(g.type === 'episode' && g.animeName === animeName && g.episodeNum === episodeNum));
  3380. })();
  3381.  
  3382. storage.videoTimes = (() => {
  3383. if (animeId !== undefined) {
  3384. const found = storage.videoTimes.find(g => g.animeId === animeId && g.episodeNum === episodeNum);
  3385. if (found !== undefined) return storage.videoTimes.filter(g => !(g.animeId === animeId && g.episodeNum === episodeNum));
  3386. }
  3387.  
  3388. return storage.videoTimes.filter(g => !(g.episodeNum === episodeNum && stringSimilarity(g.animeName, animeName) > 0.81));
  3389. })();
  3390.  
  3391. saveData(storage);
  3392. }
  3393.  
  3394. function getStoredLinkData(storage) {
  3395. if (isEpisode()) {
  3396. return storage.linkList.find(a => a.type == 'episode' && a.animeSession == animeSession && a.episodeSession == episodeSession);
  3397. }
  3398. return storage.linkList.find(a => a.type == 'anime' && a.animeSession == animeSession);
  3399. }
  3400.  
  3401. function getAnimeName() {
  3402. return isEpisode() ? /Watch (.*) - ([\d\.]+) Online/.exec($('.theatre-info h1').text())[1] : $($('.title-wrapper h1 span')[0]).text();
  3403. }
  3404.  
  3405. function getEpisodeNum() {
  3406. if (isEpisode()) return +(/Watch (.*) - ([\d\.]+) Online/.exec($('.theatre-info h1').text())[2]);
  3407. else return 0;
  3408. }
  3409.  
  3410. function sortAnimesChronologically(animeList) {
  3411. // Animes (plural)
  3412. animeList.sort((a, b) => {return getSeasonValue(a.season) > getSeasonValue(b.season) ? 1 : -1});
  3413. animeList.sort((a, b) => {return a.year > b.year ? 1 : -1});
  3414.  
  3415. return animeList;
  3416. }
  3417.  
  3418. function asyncGetResponseData(qurl) {
  3419. return new Promise((resolve, reject) => {
  3420. let req = new XMLHttpRequest();
  3421. req.open('GET', qurl, true);
  3422. req.onload = () => {
  3423. if (req.status === 200) {
  3424. resolve(JSON.parse(req.response).data);
  3425. return;
  3426. }
  3427.  
  3428. reject(undefined);
  3429. };
  3430. try {
  3431. req.send();
  3432. }
  3433. catch (err) {
  3434. console.error(err);
  3435. resolve(undefined);
  3436. }
  3437. });
  3438. }
  3439.  
  3440. function getResponseData(qurl) {
  3441. let req = new XMLHttpRequest();
  3442. req.open('GET', qurl, false);
  3443. try {
  3444. req.send();
  3445. }
  3446. catch (err) {
  3447. console.error(err);
  3448. return(undefined);
  3449. }
  3450.  
  3451. if (req.status === 200) {
  3452. return(JSON.parse(req.response).data);
  3453. }
  3454.  
  3455. return(undefined);
  3456. }
  3457.  
  3458. function getAnimeSessionFromUrl(url = window.location.toString()) {
  3459. return new RegExp('^(.*animepahe\.[a-z]+)?/(play|anime)/([^/?#]+)').exec(url)[3];
  3460. }
  3461.  
  3462. function getEpisodeSessionFromUrl(url = window.location.toString()) {
  3463. return new RegExp('^(.*animepahe\.[a-z]+)?/(play|anime)/([^/]+)/([^/?#]+)').exec(url)[4];
  3464. }
  3465.  
  3466. function makeSearchable(string) {
  3467. return encodeURIComponent(string.replace(' -',' '));
  3468. }
  3469.  
  3470. function getAnimeData(name = getAnimeName(), id = undefined, guess = false) {
  3471. const cached = (() => {
  3472. if (id !== undefined) return cachedAnimeData.find(a => a.id === id);
  3473. else return cachedAnimeData.find(a => a.title === name);
  3474. })();
  3475. if (cached !== undefined) {
  3476. return cached;
  3477. }
  3478.  
  3479. if (name.length === 0) return undefined;
  3480. const response = getResponseData('/api?m=search&q=' + makeSearchable(name));
  3481.  
  3482. if (response === undefined) return response;
  3483.  
  3484. for (const anime of response) {
  3485. if (id === undefined && anime.title === name) {
  3486. cachedAnimeData.push(anime);
  3487. return anime;
  3488. }
  3489. if (id !== undefined && anime.id === id) {
  3490. cachedAnimeData.push(anime);
  3491. return anime;
  3492. }
  3493. }
  3494.  
  3495. if (guess && response.length > 0) {
  3496. cachedAnimeData.push(response[0]);
  3497. return response[0];
  3498. }
  3499.  
  3500. return undefined;
  3501. }
  3502.  
  3503. async function asyncGetAnimeData(name = getAnimeName(), id) {
  3504. const cached = cachedAnimeData.find(a => a.id === id);
  3505. const response = cached === undefined ? await getResponseData('/api?m=search&q=' + makeSearchable(name)) : undefined;
  3506. return new Promise((resolve, reject) => {
  3507. if (cached !== undefined) {
  3508. resolve(cached);
  3509. return;
  3510. }
  3511.  
  3512. if (response === undefined) resolve(response);
  3513.  
  3514. for (const anime of response) {
  3515. if (anime.id === id) {
  3516. cachedAnimeData.push(anime);
  3517. resolve(anime);
  3518. }
  3519. }
  3520. reject(`Anime "${name}" not found`);
  3521. });
  3522. }
  3523.  
  3524. // For general animepahe pages that are not episode or anime pages
  3525. if (!url.includes('/play/') && !url.includes('/anime/') && !/anime[\/#]?[^\/]*([\?&][^=]+=[^\?^&])*$/.test(url)) {
  3526. $(`
  3527. <div id="anitracker">
  3528. </div>`).insertAfter('.notification-release');
  3529.  
  3530. addGeneralButtons();
  3531. updateSwitches();
  3532.  
  3533. return;
  3534. }
  3535.  
  3536. let animeSession = getAnimeSessionFromUrl();
  3537. let episodeSession = '';
  3538. if (isEpisode()) {
  3539. episodeSession = getEpisodeSessionFromUrl();
  3540. }
  3541.  
  3542. function getEpisodeSession(aSession, episodeNum) {
  3543. const request = new XMLHttpRequest();
  3544. request.open('GET', '/api?m=release&id=' + aSession, false);
  3545. request.send();
  3546.  
  3547. if (request.status !== 200) return undefined;
  3548.  
  3549. const response = JSON.parse(request.response);
  3550.  
  3551. return (() => {
  3552. for (let i = 1; i <= response.last_page; i++) {
  3553. const episodes = getResponseData(`/api?m=release&sort=episode_asc&page=${i}&id=${aSession}`);
  3554. if (episodes === undefined) return undefined;
  3555. const episode = episodes.find(a => a.episode === episodeNum);
  3556. if (episode === undefined) continue;
  3557. return episode.session;
  3558. }
  3559. })();
  3560. }
  3561.  
  3562. function refreshSession(from404 = false) {
  3563. /* Return codes:
  3564. * 0: ok!
  3565. * 1: couldn't find stored session at 404 page
  3566. * 2: couldn't get anime data
  3567. * 3: couldn't get episode session
  3568. * 4: idk
  3569. */
  3570.  
  3571. const storage = getStorage();
  3572. const bobj = getStoredLinkData(storage);
  3573.  
  3574. let name = '';
  3575. let episodeNum = 0;
  3576.  
  3577. if (bobj === undefined && from404) return 1;
  3578.  
  3579. if (bobj !== undefined) {
  3580. name = bobj.animeName;
  3581. episodeNum = bobj.episodeNum;
  3582. }
  3583. else {
  3584. name = getAnimeName();
  3585. episodeNum = getEpisodeNum();
  3586. }
  3587.  
  3588. if (isEpisode()) {
  3589. const animeData = getAnimeData(name, bobj?.animeId, true);
  3590.  
  3591. if (animeData === undefined) return 2;
  3592.  
  3593. if (bobj?.animeId === undefined && animeData.title !== name && !refreshGuessWarning(name, animeData.title)) {
  3594. return 2;
  3595. }
  3596.  
  3597. const episodeSession = getEpisodeSession(animeData.session, episodeNum);
  3598.  
  3599. if (episodeSession === undefined) return 3;
  3600.  
  3601. if (bobj !== undefined) {
  3602. storage.linkList = storage.linkList.filter(g => !(g.type === 'episode' && g.animeSession === bobj.animeSession && g.episodeSession === bobj.episodeSession));
  3603. }
  3604.  
  3605. saveData(storage);
  3606.  
  3607. window.location.replace('/play/' + animeData.session + '/' + episodeSession + window.location.search);
  3608.  
  3609. return 0;
  3610. }
  3611. else if (bobj !== undefined && bobj.animeId !== undefined) {
  3612. storage.linkList = storage.linkList.filter(g => !(g.type === 'anime' && g.animeSession === bobj.animeSession));
  3613.  
  3614. saveData(storage);
  3615.  
  3616. window.location.replace('/a/' + bobj.animeId);
  3617. return 0;
  3618. }
  3619. else {
  3620. if (bobj !== undefined) {
  3621. storage.linkList = storage.linkList.filter(g => !(g.type === 'anime' && g.animeSession === bobj.animeSession));
  3622. saveData(storage);
  3623. }
  3624.  
  3625. let animeData = getAnimeData(name, undefined, true);
  3626.  
  3627. if (animeData === undefined) return 2;
  3628.  
  3629. if (animeData.title !== name && !refreshGuessWarning(name, animeData.title)) {
  3630. return 2;
  3631. }
  3632.  
  3633. window.location.replace('/a/' + animeData.id);
  3634. return 0;
  3635. }
  3636.  
  3637. return 4;
  3638. }
  3639.  
  3640. function refreshGuessWarning(name, title) {
  3641. return confirm(`[AnimePahe Improvements]\n\nAn exact match with the anime name "${name}" couldn't be found. Go to "${title}" instead?`);
  3642. }
  3643.  
  3644. const obj = getStoredLinkData(initialStorage);
  3645.  
  3646. if (isEpisode() && !is404) $('#downloadMenu').changeElementType('button');
  3647.  
  3648. console.log('[AnimePahe Improvements]', obj, animeSession, episodeSession);
  3649.  
  3650. function setSessionData() {
  3651. const animeName = getAnimeName();
  3652.  
  3653. const storage = getStorage();
  3654. if (isEpisode()) {
  3655. storage.linkList.push({
  3656. animeId: getAnimeData(animeName)?.id,
  3657. animeSession: animeSession,
  3658. episodeSession: episodeSession,
  3659. type: 'episode',
  3660. animeName: animeName,
  3661. episodeNum: getEpisodeNum()
  3662. });
  3663. }
  3664. else {
  3665. storage.linkList.push({
  3666. animeId: getAnimeData(animeName)?.id,
  3667. animeSession: animeSession,
  3668. type: 'anime',
  3669. animeName: animeName
  3670. });
  3671. }
  3672. if (storage.linkList.length > 1000) {
  3673. storage.splice(0,1);
  3674. }
  3675.  
  3676. saveData(storage);
  3677. }
  3678.  
  3679. if (obj === undefined && !is404) {
  3680. if (!isRandomAnime()) setSessionData();
  3681. }
  3682. else if (obj !== undefined && is404) {
  3683. document.title = "Refreshing session... :: animepahe";
  3684. $('.text-center h1').text('Refreshing session, please wait...');
  3685. const code = refreshSession(true);
  3686. if (code === 1) {
  3687. $('.text-center h1').text('Couldn\'t refresh session: Link not found in tracker');
  3688. }
  3689. else if (code === 2) {
  3690. $('.text-center h1').text('Couldn\'t refresh session: Couldn\'t get anime data');
  3691. }
  3692. else if (code === 3) {
  3693. $('.text-center h1').text('Couldn\'t refresh session: Couldn\'t get episode data');
  3694. }
  3695. else if (code !== 0) {
  3696. $('.text-center h1').text('Couldn\'t refresh session: An unknown error occured');
  3697. }
  3698.  
  3699. if ([2,3].includes(code)) {
  3700. if (obj.episodeNum !== undefined) {
  3701. $(`<h3>
  3702. Try finding the episode using the following info:
  3703. <br>Anime name: ${obj.animeName}
  3704. <br>Episode: ${obj.episodeNum}
  3705. </h3>`).insertAfter('.text-center h1');
  3706. }
  3707. else {
  3708. $(`<h3>
  3709. Try finding the anime using the following info:
  3710. <br>Anime name: ${obj.animeName}
  3711. </h3>`).insertAfter('.text-center h1');
  3712. }
  3713. }
  3714. return;
  3715. }
  3716. else if (obj === undefined && is404) {
  3717. if (document.referrer.length > 0) {
  3718. const bobj = (() => {
  3719. if (!/\/play\/.+/.test(document.referrer) && !/\/anime\/.+/.test(document.referrer)) {
  3720. return true;
  3721. }
  3722. const session = getAnimeSessionFromUrl(document.referrer);
  3723. if (isEpisode(document.referrer)) {
  3724. return initialStorage.linkList.find(a => a.type === 'episode' && a.animeSession === session && a.episodeSession === getEpisodeSessionFromUrl(document.referrer));
  3725. }
  3726. else {
  3727. return initialStorage.linkList.find(a => a.type === 'anime' && a.animeSession === session);
  3728. }
  3729. })();
  3730. if (bobj !== undefined) {
  3731. const prevUrl = new URL(document.referrer);
  3732. const params = new URLSearchParams(prevUrl);
  3733. params.set('ref','404');
  3734. prevUrl.search = params.toString();
  3735. windowOpen(prevUrl.toString(), '_self');
  3736. return;
  3737. }
  3738. }
  3739. $('.text-center h1').text('Cannot refresh session: Link not stored in tracker');
  3740. return;
  3741. }
  3742.  
  3743. function getSubInfo(str) {
  3744. const match = /^\b([^·]+)·\s*(\d{2,4})p(.*)$/.exec(str);
  3745. return {
  3746. name: match[1],
  3747. quality: +match[2],
  3748. other: match[3]
  3749. };
  3750. }
  3751.  
  3752. // Set the quality to best automatically
  3753. function bestVideoQuality() {
  3754. if (!isEpisode()) return;
  3755.  
  3756. const currentSub = getStoredLinkData(getStorage()).subInfo || getSubInfo($('#resolutionMenu .active').text());
  3757.  
  3758. let index = -1;
  3759. for (let i = 0; i < $('#resolutionMenu').children().length; i++) {
  3760. const sub = $('#resolutionMenu').children()[i];
  3761. const subInfo = getSubInfo($(sub).text());
  3762. if (subInfo.name !== currentSub.name || subInfo.other !== currentSub.other) continue;
  3763.  
  3764. if (subInfo.quality >= currentSub.quality) index = i;
  3765. }
  3766.  
  3767. if (index === -1) {
  3768. return;
  3769. }
  3770.  
  3771. const newSub = $('#resolutionMenu').children()[index];
  3772.  
  3773.  
  3774. if (!["","Loading..."].includes($('#fansubMenu').text())) {
  3775. if ($(newSub).text() === $('#resolutionMenu .active').text()) return;
  3776. newSub.click();
  3777. return;
  3778. }
  3779.  
  3780. new MutationObserver(function(mutationList, observer) {
  3781. newSub.click();
  3782. observer.disconnect();
  3783. }).observe($('#fansubMenu')[0], { childList: true });
  3784. }
  3785.  
  3786. function setIframeUrl(url) {
  3787. $('.embed-responsive-item').remove();
  3788. $(`
  3789. <iframe class="embed-responsive-item" scrolling="no" allowfullscreen="" allowtransparency="" src="${url}"></iframe>
  3790. `).prependTo('.embed-responsive');
  3791. $('.embed-responsive-item')[0].contentWindow.focus();
  3792. }
  3793.  
  3794. // Fix the quality dropdown buttons
  3795. if (isEpisode()) {
  3796. new MutationObserver(function(mutationList, observer) {
  3797. $('.click-to-load').remove();
  3798. $('#resolutionMenu').off('click');
  3799. $('#resolutionMenu').on('click', (el) => {
  3800. const targ = $(el.target);
  3801.  
  3802. if (targ.data('src') === undefined) return;
  3803.  
  3804. setIframeUrl(targ.data('src'));
  3805.  
  3806. $('#resolutionMenu .active').removeClass('active');
  3807. targ.addClass('active');
  3808.  
  3809. $('#fansubMenu').html(targ.html());
  3810.  
  3811. const storage = getStorage();
  3812. const data = getStoredLinkData(storage);
  3813. data.subInfo = getSubInfo(targ.text());
  3814. saveData(storage);
  3815.  
  3816. $.cookie('res', targ.data('resolution'), {
  3817. expires: 365,
  3818. path: '/'
  3819. });
  3820. $.cookie('aud', targ.data('audio'), {
  3821. expires: 365,
  3822. path: '/'
  3823. });
  3824. $.cookie('av1', targ.data('av1'), {
  3825. expires: 365,
  3826. path: '/'
  3827. });
  3828. });
  3829. observer.disconnect();
  3830. }).observe($('#fansubMenu')[0], { childList: true });
  3831.  
  3832.  
  3833.  
  3834. if (initialStorage.bestQuality === true) {
  3835. bestVideoQuality();
  3836. }
  3837. else if (!["","Loading..."].includes($('#fansubMenu').text())) {
  3838. $('#resolutionMenu .active').click();
  3839. } else {
  3840. new MutationObserver(function(mutationList, observer) {
  3841. $('#resolutionMenu .active').click();
  3842. observer.disconnect();
  3843. }).observe($('#fansubMenu')[0], { childList: true });
  3844. }
  3845.  
  3846. const timeArg = paramArray.find(a => a[0] === 'time');
  3847. if (timeArg !== undefined) {
  3848. applyTimeArg(timeArg);
  3849. }
  3850. }
  3851.  
  3852. function applyTimeArg(timeArg) {
  3853. const time = timeArg[1];
  3854.  
  3855. function check() {
  3856. if ($('.embed-responsive-item').attr('src') !== undefined) done();
  3857. else setTimeout(check, 100);
  3858. }
  3859. setTimeout(check, 100);
  3860.  
  3861. function done() {
  3862. setIframeUrl(stripUrl($('.embed-responsive-item').attr('src')) + '?time=' + time);
  3863.  
  3864. window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
  3865. }
  3866. }
  3867.  
  3868.  
  3869. function getTrackerDiv() {
  3870. return $(`
  3871. <div id="anitracker">
  3872. <button class="btn btn-dark" id="anitracker-refresh-session" title="Refresh the session for the current page">
  3873. <i class="fa fa-refresh" aria-hidden="true"></i>
  3874. &nbsp;Refresh Session
  3875. </button>
  3876. </div>`);
  3877. }
  3878.  
  3879. async function asyncGetAllEpisodes(session, sort = "asc") {
  3880. const episodeList = [];
  3881. const request = new XMLHttpRequest();
  3882. request.open('GET', `/api?m=release&sort=episode_${sort}&id=` + session, true);
  3883.  
  3884. return new Promise((resolve, reject) => {
  3885. request.onload = () => {
  3886. if (request.status !== 200) {
  3887. reject("Received response code " + request.status);
  3888. return;
  3889. }
  3890.  
  3891. const response = JSON.parse(request.response);
  3892. if (response.current_page === response.last_page) {
  3893. episodeList.push(...response.data);
  3894. }
  3895. else for (let i = 1; i <= response.last_page; i++) {
  3896. asyncGetResponseData(`/api?m=release&sort=episode_${sort}&page=${i}&id=${session}`).then((episodes) => {
  3897. if (episodes === undefined || episodes.length === 0) return;
  3898. episodeList.push(...episodes);
  3899. });
  3900. }
  3901. resolve(episodeList);
  3902. };
  3903. request.send();
  3904. });
  3905. }
  3906.  
  3907. async function getRelationData(session, relationType) {
  3908. const request = new XMLHttpRequest();
  3909. request.open('GET', '/anime/' + session, false);
  3910. request.send();
  3911.  
  3912. const page = request.status === 200 ? $(request.response) : {};
  3913.  
  3914. if (Object.keys(page).length === 0) return undefined;
  3915.  
  3916. const relationDiv = (() => {
  3917. for (const div of page.find('.anime-relation .col-12')) {
  3918. if ($(div).find('h4 span').text() !== relationType) continue;
  3919. return $(div);
  3920. break;
  3921. }
  3922. return undefined;
  3923. })();
  3924.  
  3925. if (relationDiv === undefined) return undefined;
  3926.  
  3927. const relationSession = new RegExp('^.*animepahe\.[a-z]+/anime/([^/]+)').exec(relationDiv.find('a')[0].href)[1];
  3928.  
  3929. return new Promise(resolve => {
  3930. const episodeList = [];
  3931. asyncGetAllEpisodes(relationSession).then((episodes) => {
  3932. episodeList.push(...episodes);
  3933.  
  3934. if (episodeList.length === 0) {
  3935. resolve(undefined);
  3936. return;
  3937. }
  3938.  
  3939. resolve({
  3940. episodes: episodeList,
  3941. name: $(relationDiv.find('h5')[0]).text(),
  3942. poster: relationDiv.find('img').attr('data-src').replace('.th',''),
  3943. session: relationSession
  3944. });
  3945. });
  3946.  
  3947. });
  3948. }
  3949.  
  3950. function hideSpinner(t, parents = 1) {
  3951. $(t).parents(`:eq(${parents})`).find('.anitracker-download-spinner').hide();
  3952. }
  3953.  
  3954. if (isEpisode()) {
  3955. getTrackerDiv().appendTo('.anime-note');
  3956.  
  3957. $('.prequel,.sequel').addClass('anitracker-thumbnail');
  3958.  
  3959. $(`
  3960. <span relationType="Prequel" class="dropdown-item anitracker-relation-link" id="anitracker-prequel-link">
  3961. Previous Anime
  3962. </span>`).prependTo('.episode-menu #scrollArea');
  3963.  
  3964. $(`
  3965. <span relationType="Sequel" class="dropdown-item anitracker-relation-link" id="anitracker-sequel-link">
  3966. Next Anime
  3967. </span>`).appendTo('.episode-menu #scrollArea');
  3968.  
  3969. $('.anitracker-relation-link').on('click', function() {
  3970. if (this.href !== undefined) {
  3971. $(this).off();
  3972. return;
  3973. }
  3974.  
  3975. $(this).parents(':eq(2)').find('.anitracker-download-spinner').show();
  3976.  
  3977. const animeData = getAnimeData();
  3978.  
  3979. if (animeData === undefined) {
  3980. hideSpinner(this, 2);
  3981. return;
  3982. }
  3983.  
  3984. const relationType = $(this).attr('relationType');
  3985. getRelationData(animeData.session, relationType).then((relationData) => {
  3986. if (relationData === undefined) {
  3987. hideSpinner(this, 2);
  3988. alert(`[AnimePahe Improvements]\n\nNo ${relationType.toLowerCase()} found for this anime.`);
  3989. $(this).remove();
  3990. return;
  3991. }
  3992.  
  3993. const episodeSession = relationType === 'Prequel' ? relationData.episodes[relationData.episodes.length-1].session : relationData.episodes[0].session;
  3994.  
  3995. windowOpen(`/play/${relationData.session}/${episodeSession}`, '_self');
  3996. hideSpinner(this, 2);
  3997. });
  3998.  
  3999. });
  4000.  
  4001. if ($('.prequel').length === 0) setPrequelPoster();
  4002. if ($('.sequel').length === 0) setSequelPoster();
  4003. } else {
  4004. getTrackerDiv().insertAfter('.anime-content');
  4005. }
  4006.  
  4007. async function setPrequelPoster() {
  4008. const relationData = await getRelationData(animeSession, 'Prequel');
  4009. if (relationData === undefined) {
  4010. $('#anitracker-prequel-link').remove();
  4011. return;
  4012. }
  4013. const relationLink = `/play/${relationData.session}/${relationData.episodes[relationData.episodes.length-1].session}`;
  4014. $(`
  4015. <div class="prequel hidden-sm-down anitracker-thumbnail">
  4016. <a href="${relationLink}" title="${toHtmlCodes("Play Last Episode of " + relationData.name)}">
  4017. <img style="filter: none;" src="${relationData.poster}" data-src="${relationData.poster}" alt="">
  4018. </a>
  4019. <i class="fa fa-chevron-left" aria-hidden="true"></i>
  4020. </div>`).appendTo('.player');
  4021.  
  4022. $('#anitracker-prequel-link').attr('href', relationLink);
  4023. $('#anitracker-prequel-link').text(relationData.name);
  4024. $('#anitracker-prequel-link').changeElementType('a');
  4025.  
  4026. // If auto-clear is on, delete this prequel episode from the tracker
  4027. if (getStorage().autoDelete === true) {
  4028. deleteEpisodesFromTracker(undefined, relationData.name);
  4029. }
  4030. }
  4031.  
  4032. async function setSequelPoster() {
  4033. const relationData = await getRelationData(animeSession, 'Sequel');
  4034. if (relationData === undefined) {
  4035. $('#anitracker-sequel-link').remove();
  4036. return;
  4037. }
  4038. const relationLink = `/play/${relationData.session}/${relationData.episodes[0].session}`;
  4039. $(`
  4040. <div class="sequel hidden-sm-down anitracker-thumbnail">
  4041. <a href="${relationLink}" title="${toHtmlCodes("Play First Episode of " + relationData.name)}">
  4042. <img style="filter: none;" src="${relationData.poster}" data-src="${relationData.poster}" alt="">
  4043. </a>
  4044. <i class="fa fa-chevron-right" aria-hidden="true"></i>
  4045. </div>`).appendTo('.player');
  4046.  
  4047. $('#anitracker-sequel-link').attr('href', relationLink);
  4048. $('#anitracker-sequel-link').text(relationData.name);
  4049. $('#anitracker-sequel-link').changeElementType('a');
  4050. }
  4051.  
  4052. if (!isEpisode() && $('#anitracker') != undefined) {
  4053. $('#anitracker').attr('style', "max-width: 1100px;margin-left: auto;margin-right: auto;margin-bottom: 20px;");
  4054. }
  4055.  
  4056. $('#anitracker-refresh-session').on('click', function(e) {
  4057. const elem = $('#anitracker-refresh-session');
  4058. let timeout = temporaryHtmlChange(elem, 2200, 'Waiting...');
  4059.  
  4060. const result = refreshSession();
  4061.  
  4062. if (result === 0) {
  4063. temporaryHtmlChange(elem, 2200, '<i class="fa fa-refresh" aria-hidden="true" style="animation: anitracker-spin 1s linear infinite;"></i>&nbsp;&nbsp;Refreshing...', timeout);
  4064. }
  4065. else if ([2,3].includes(result)) {
  4066. temporaryHtmlChange(elem, 2200, 'Failed: Couldn\'t find session', timeout);
  4067. }
  4068. else {
  4069. temporaryHtmlChange(elem, 2200, 'Failed.', timeout);
  4070. }
  4071. });
  4072.  
  4073. if (isEpisode()) {
  4074. // Replace the download buttons with better ones
  4075. if ($('#pickDownload a').length > 0) replaceDownloadButtons();
  4076. else {
  4077. new MutationObserver(function(mutationList, observer) {
  4078. replaceDownloadButtons();
  4079. observer.disconnect();
  4080. }).observe($('#pickDownload')[0], { childList: true });
  4081. }
  4082.  
  4083.  
  4084. $(document).on('blur', () => {
  4085. $('.dropdown-menu.show').removeClass('show');
  4086. });
  4087.  
  4088. (() => {
  4089. const storage = getStorage();
  4090. const foundNotifEpisode = storage.notifications.episodes.find(a => a.session === episodeSession);
  4091. if (foundNotifEpisode !== undefined) {
  4092. foundNotifEpisode.watched = true;
  4093. saveData(storage);
  4094. }
  4095. })();
  4096. }
  4097.  
  4098. function replaceDownloadButtons() {
  4099. for (const aTag of $('#pickDownload a')) {
  4100. $(aTag).changeElementType('span');
  4101. }
  4102.  
  4103. $('#pickDownload span').on('click', function(e) {
  4104.  
  4105. let request = new XMLHttpRequest();
  4106. //request.open('GET', `https://opsalar.000webhostapp.com/animepahe.php?url=${$(this).attr('href')}`, true);
  4107. request.open('GET', $(this).attr('href'), true);
  4108. try {
  4109. request.send();
  4110. $(this).parents(':eq(1)').find('.anitracker-download-spinner').show();
  4111. }
  4112. catch (err) {
  4113. windowOpen($(this).attr('href'));
  4114. }
  4115.  
  4116. const dlBtn = $(this);
  4117.  
  4118. request.onload = function(e) {
  4119. hideSpinner(dlBtn);
  4120. if (request.readyState !== 4 || request.status !== 200 ) {
  4121. windowOpen(dlBtn.attr('href'));
  4122. return;
  4123. }
  4124.  
  4125. const htmlText = request.response;
  4126. const link = /https:\/\/kwik.\w+\/f\/[^"]+/.exec(htmlText);
  4127. if (link) {
  4128. dlBtn.attr('href', link[0]);
  4129. dlBtn.off();
  4130. dlBtn.changeElementType('a');
  4131. windowOpen(link[0]);
  4132. }
  4133. else windowOpen(dlBtn.attr('href'));
  4134.  
  4135. };
  4136. });
  4137. }
  4138.  
  4139. function stripUrl(url) {
  4140. if (url === undefined) {
  4141. console.error('[AnimePahe Improvements] stripUrl was used with undefined URL');
  4142. return url;
  4143. }
  4144. const loc = new URL(url);
  4145. return loc.origin + loc.pathname;
  4146. }
  4147.  
  4148. function temporaryHtmlChange(elem, delay, html, timeout = undefined) {
  4149. if (timeout !== undefined) clearTimeout(timeout);
  4150. if ($(elem).attr('og-html') === undefined) {
  4151. $(elem).attr('og-html', $(elem).html());
  4152. }
  4153. elem.html(html);
  4154. return setTimeout(() => {
  4155. $(elem).html($(elem).attr('og-html'));
  4156. }, delay);
  4157. }
  4158.  
  4159. $(`
  4160. <button class="btn btn-dark" id="anitracker-clear-from-tracker" title="Remove this page from the session tracker">
  4161. <i class="fa fa-trash" aria-hidden="true"></i>
  4162. &nbsp;Clear from Tracker
  4163. </button>`).appendTo('#anitracker');
  4164.  
  4165. $('#anitracker-clear-from-tracker').on('click', function() {
  4166. const animeName = getAnimeName();
  4167.  
  4168. if (isEpisode()) {
  4169. deleteEpisodeFromTracker(animeName, getEpisodeNum(), getAnimeData().id);
  4170.  
  4171. if ($('.embed-responsive-item').length > 0) {
  4172. const storage = getStorage();
  4173. const videoUrl = stripUrl($('.embed-responsive-item').attr('src'));
  4174. for (const videoData of storage.videoTimes) {
  4175. if (!videoData.videoUrls.includes(videoUrl)) continue;
  4176. const index = storage.videoTimes.indexOf(videoData);
  4177. storage.videoTimes.splice(index, 1);
  4178. saveData(storage);
  4179. break;
  4180. }
  4181. }
  4182. }
  4183. else {
  4184. const storage = getStorage();
  4185.  
  4186. storage.linkList = storage.linkList.filter(a => !(a.type === 'anime' && a.animeName === animeName));
  4187.  
  4188. saveData(storage);
  4189. }
  4190.  
  4191. temporaryHtmlChange($('#anitracker-clear-from-tracker'), 1500, 'Cleared!');
  4192. });
  4193.  
  4194. function setCoverBlur(img) {
  4195. const cover = $('.anime-cover');
  4196. const ratio = cover.width()/img.width;
  4197. if (ratio <= 1) return;
  4198. cover.css('filter', `blur(${(ratio*Math.max((img.height/img.width)**2, 1))*1.6}px)`);
  4199. }
  4200.  
  4201. function improvePoster() {
  4202. if ($('.anime-poster .youtube-preview').length === 0) {
  4203. $('.anime-poster .poster-image').attr('target','_blank');
  4204. return;
  4205. }
  4206. $('.anime-poster .youtube-preview').removeAttr('href');
  4207. $(`
  4208. <a style="display:block;" target="_blank" href="${$('.anime-poster img').attr('src')}">
  4209. View full poster
  4210. </a>`).appendTo('.anime-poster');
  4211. }
  4212.  
  4213. if (isAnime()) {
  4214. if ($('.anime-poster img').attr('src') !== undefined) {
  4215. improvePoster();
  4216. }
  4217. else $('.anime-poster img').on('load', (e) => {
  4218. improvePoster();
  4219. $(e.target).off('load');
  4220. });
  4221.  
  4222. $(`
  4223. <button class="btn btn-dark" id="anitracker-clear-episodes-from-tracker" title="Clear all episodes from this anime from the session tracker">
  4224. <i class="fa fa-trash" aria-hidden="true"></i>
  4225. <i class="fa fa-window-maximize" aria-hidden="true"></i>
  4226. &nbsp;Clear Episodes from Tracker
  4227. </button>`).appendTo('#anitracker');
  4228.  
  4229. $('#anitracker-clear-episodes-from-tracker').on('click', function() {
  4230. const animeData = getAnimeData();
  4231. deleteEpisodesFromTracker(undefined, animeData.title, animeData.id);
  4232.  
  4233. temporaryHtmlChange($('#anitracker-clear-episodes-from-tracker'), 1500, 'Cleared!');
  4234. });
  4235.  
  4236. const storedObj = getStoredLinkData(initialStorage);
  4237.  
  4238. if (storedObj === undefined || storedObj?.coverImg === undefined) updateAnimeCover();
  4239. else
  4240. {
  4241. new MutationObserver(function(mutationList, observer) {
  4242. $('.anime-cover').css('background-image', `url("${storedObj.coverImg}")`);
  4243. $('.anime-cover').addClass('anitracker-replaced-cover');
  4244. const img = new Image();
  4245. img.src = storedObj.coverImg;
  4246. img.onload = () => {
  4247. setCoverBlur(img);
  4248. };
  4249. observer.disconnect();
  4250. }).observe($('.anime-cover')[0], { attributes: true });
  4251. }
  4252.  
  4253. if (isRandomAnime()) {
  4254. const sourceParams = new URLSearchParams(window.location.search);
  4255. window.history.replaceState({}, document.title, "/anime/" + animeSession);
  4256.  
  4257. const storage = getStorage();
  4258. if (storage.cache) {
  4259. for (const [key, value] of Object.entries(storage.cache)) {
  4260. filterSearchCache[key] = value;
  4261. }
  4262. delete storage.cache;
  4263. saveData(storage);
  4264. }
  4265.  
  4266. $(`
  4267. <div style="margin-left: 240px;">
  4268. <div class="btn-group">
  4269. <button class="btn btn-dark" id="anitracker-reroll-button"><i class="fa fa-random" aria-hidden="true"></i>&nbsp;Reroll Anime</button>
  4270. </div>
  4271. <div class="btn-group">
  4272. <button class="btn btn-dark" id="anitracker-save-session"><i class="fa fa-floppy-o" aria-hidden="true"></i>&nbsp;Save Session</button>
  4273. </div>
  4274. </div>`).appendTo('.title-wrapper');
  4275.  
  4276. $('#anitracker-reroll-button').on('click', function() {
  4277. $(this).text('Rerolling...');
  4278.  
  4279. const sourceFilters = new URLSearchParams(sourceParams.toString());
  4280. getFilteredList(filterListFromParams(sourceFilters, false)).then((animeList) => {
  4281. storage.cache = filterSearchCache;
  4282. saveData(storage);
  4283.  
  4284. if (sourceParams.has('search')) {
  4285. $.getScript('https://cdn.jsdelivr.net/npm/fuse.js@7.0.0', function() {
  4286. getRandomAnime(searchList(Fuse, animeList, decodeURIComponent(sourceParams.get('search'))), '?' + sourceParams.toString(), '_self');
  4287. });
  4288. }
  4289. else {
  4290. getRandomAnime(animeList, '?' + sourceParams.toString(), '_self');
  4291. }
  4292. });
  4293.  
  4294. });
  4295.  
  4296. $('#anitracker-save-session').on('click', function() {
  4297. setSessionData();
  4298. $('#anitracker-save-session').off();
  4299. $(this).text('Saved!');
  4300.  
  4301. setTimeout(() => {
  4302. $(this).parent().remove();
  4303. }, 1500);
  4304. });
  4305. }
  4306.  
  4307. new MutationObserver(function(mutationList, observer) {
  4308. const pageNum = (() => {
  4309. const elem = $('.pagination');
  4310. if (elem.length == 0) return 1;
  4311. return +/^(\d+)/.exec($('.pagination').find('.page-item.active span').text())[0];
  4312. })();
  4313.  
  4314. const episodeSort = $('.episode-bar .btn-group-toggle .active').text().trim();
  4315.  
  4316. const episodes = getResponseData(`/api?m=release&sort=episode_${episodeSort}&page=${pageNum}&id=${animeSession}`);
  4317. if (episodes === undefined) return undefined;
  4318.  
  4319. const episodeElements = $('.episode-wrap');
  4320.  
  4321. for (let i = 0; i < episodeElements.length; i++) {
  4322. const elem = $(episodeElements[i]);
  4323.  
  4324. const date = new Date(episodes[i].created_at + " UTC");
  4325.  
  4326. $(`
  4327. <a class="anitracker-episode-time" href="${$(elem.find('a.play')).attr('href')}" tabindex="-1" title="${date.toDateString() + " " + date.toLocaleTimeString()}">${date.toLocaleDateString()}</a>
  4328. `).appendTo(elem.find('.episode-title-wrap'));
  4329. }
  4330. observer.disconnect();
  4331. setTimeout(observer.observe($('.episode-list-wrapper')[0], { childList: true, subtree: true }), 1);
  4332. }).observe($('.episode-list-wrapper')[0], { childList: true, subtree: true });
  4333.  
  4334. // Bookmark icon
  4335. const animename = getAnimeName();
  4336. const animeid = getAnimeData(animename).id;
  4337. $('h1 .fa').remove();
  4338.  
  4339. const notifIcon = (() => {
  4340. if (initialStorage.notifications.anime.find(a => a.name === animename) !== undefined) return true;
  4341. for (const info of $('.anime-info p>strong')) {
  4342. if (!$(info).text().startsWith('Status:')) continue;
  4343. return $(info).text().includes("Not yet aired") || $(info).find('a').text() === "Currently Airing";
  4344. }
  4345. return false;
  4346. })() ?
  4347. `<i title="Add to episode feed" class="fa fa-bell anitracker-title-icon anitracker-notifications-toggle">
  4348. <i style="display: none;" class="fa fa-check anitracker-title-icon-check" aria-hidden="true"></i>
  4349. </i>` : '';
  4350.  
  4351. $(`
  4352. <i title="Bookmark this anime" class="fa fa-bookmark anitracker-title-icon anitracker-bookmark-toggle">
  4353. <i style="display: none;" class="fa fa-check anitracker-title-icon-check" aria-hidden="true"></i>
  4354. </i>${notifIcon}<a href="/a/${animeid}" title="Get Link" class="fa fa-link btn anitracker-title-icon" data-toggle="modal" data-target="#modalBookmark"></a>
  4355. `).appendTo('.title-wrapper>h1');
  4356.  
  4357. if (initialStorage.bookmarks.find(g => g.id === animeid) !== undefined) {
  4358. $('.anitracker-bookmark-toggle .anitracker-title-icon-check').show();
  4359. }
  4360.  
  4361. if (initialStorage.notifications.anime.find(g => g.id === animeid) !== undefined) {
  4362. $('.anitracker-notifications-toggle .anitracker-title-icon-check').show();
  4363. }
  4364.  
  4365. $('.anitracker-bookmark-toggle').on('click', (e) => {
  4366. const check = $(e.currentTarget).find('.anitracker-title-icon-check');
  4367.  
  4368. if (toggleBookmark(animeid, animename)) {
  4369. check.show();
  4370. return;
  4371. }
  4372. check.hide();
  4373.  
  4374. });
  4375.  
  4376. $('.anitracker-notifications-toggle').on('click', (e) => {
  4377. const check = $(e.currentTarget).find('.anitracker-title-icon-check');
  4378.  
  4379. if (toggleNotifications(animename, animeid)) {
  4380. check.show();
  4381. return;
  4382. }
  4383. check.hide();
  4384.  
  4385. });
  4386. }
  4387.  
  4388. function getRandomAnime(list, args, openType = '_blank') {
  4389. if (list.length === 0) {
  4390. alert("[AnimePahe Improvements]\n\nThere is no anime that matches the selected filters.");
  4391. return;
  4392. }
  4393. const random = randint(0, list.length-1);
  4394. windowOpen(list[random].link + args, openType);
  4395. }
  4396.  
  4397. function isRandomAnime() {
  4398. return new URLSearchParams(window.location.search).has('anitracker-random');
  4399. }
  4400.  
  4401. function getBadCovers() {
  4402. const storage = getStorage();
  4403. return ['https://s.pximg.net/www/images/pixiv_logo.png',
  4404. 'https://st.deviantart.net/minish/main/logo/card_black_large.png',
  4405. 'https://www.wcostream.com/wp-content/themes/animewp78712/images/logo.gif',
  4406. 'https://s.pinimg.com/images/default_open_graph',
  4407. 'https://share.redd.it/preview/post/',
  4408. 'https://i.redd.it/o0h58lzmax6a1.png',
  4409. 'https://ir.ebaystatic.com/cr/v/c1/ebay-logo',
  4410. 'https://i.ebayimg.com/images/g/7WgAAOSwQ7haxTU1/s-l1600.jpg',
  4411. 'https://www.rottentomatoes.com/assets/pizza-pie/head-assets/images/RT_TwitterCard',
  4412. 'https://m.media-amazon.com/images/G/01/social_share/amazon_logo',
  4413. 'https://zoro.to/images/capture.png',
  4414. 'https://cdn.myanimelist.net/img/sp/icon/twitter-card.png',
  4415. 'https://s2.bunnycdn.ru/assets/sites/animesuge/images/preview.jpg',
  4416. 'https://s2.bunnycdn.ru/assets/sites/anix/preview.jpg',
  4417. 'https://cdn.myanimelist.net/images/company_no_picture.png',
  4418. 'https://myanimeshelf.com/eva2/handlers/generateSocialImage.php',
  4419. 'https://cdn.myanimelist.net/img/sp/icon/apple-touch-icon',
  4420. 'https://m.media-amazon.com/images/G/01/imdb/images/social',
  4421. 'https://forums.animeuknews.net/styles/default/',
  4422. 'https://honeysanime.com/wp-content/uploads/2016/12/facebook_cover_2016_851x315.jpg',
  4423. 'https://fi.somethingawful.com/images/logo.png',
  4424. ...storage.badCovers];
  4425. }
  4426.  
  4427. async function updateAnimeCover() {
  4428. $(`<div id="anitracker-cover-spinner">
  4429. <div class="spinner-border text-danger" role="status">
  4430. <span class="sr-only">Loading...</span>
  4431. </div>
  4432. </div>`).prependTo('.anime-cover');
  4433.  
  4434. const request = new XMLHttpRequest();
  4435. let beforeYear = 2022;
  4436. for (const info of $('.anime-info p')) {
  4437. if (!$(info).find('strong').html().startsWith('Season:')) continue;
  4438. const year = +/(\d+)$/.exec($(info).find('a').text())[0];
  4439. if (year >= beforeYear) beforeYear = year + 1;
  4440. }
  4441. 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);
  4442. request.onload = function() {
  4443. if (request.status !== 200) {
  4444. $('#anitracker-cover-spinner').remove();
  4445. return;
  4446. }
  4447. if ($('.anime-cover').css('background-image').length > 10) {
  4448. decideAnimeCover(request.response);
  4449. }
  4450. else {
  4451. new MutationObserver(function(mutationList, observer) {
  4452. if ($('.anime-cover').css('background-image').length <= 10) return;
  4453. decideAnimeCover(request.response);
  4454. observer.disconnect();
  4455. }).observe($('.anime-cover')[0], { attributes: true });
  4456. }
  4457. };
  4458. request.send();
  4459. }
  4460.  
  4461. function trimHttp(string) {
  4462. return string.replace(/^https?:\/\//,'');
  4463. }
  4464.  
  4465. async function setAnimeCover(src) {
  4466. return new Promise(resolve => {
  4467. $('.anime-cover').css('background-image', `url("${storedObj.coverImg}")`);
  4468. $('.anime-cover').addClass('anitracker-replaced-cover');
  4469. const img = new Image();
  4470. img.src = src;
  4471. img.onload = () => {
  4472. setCoverBlur(img);
  4473. }
  4474.  
  4475. $('.anime-cover').addClass('anitracker-replaced-cover');
  4476. $('.anime-cover').css('background-image', `url("${src}")`);
  4477. $('.anime-cover').attr('image', src);
  4478.  
  4479. $('#anitracker-replace-cover').remove();
  4480. $(`<button class="btn btn-dark" id="anitracker-replace-cover" title="Use another cover instead">
  4481. <i class="fa fa-refresh" aria-hidden="true"></i>
  4482. </button>`).appendTo('.anime-cover');
  4483.  
  4484. $('#anitracker-replace-cover').on('click', e => {
  4485. const storage = getStorage();
  4486. storage.badCovers.push($('.anime-cover').attr('image'));
  4487. saveData(storage);
  4488. updateAnimeCover();
  4489. $(e.target).off();
  4490. playAnimation($(e.target).find('i'), 'spin', 'infinite', 1);
  4491. });
  4492.  
  4493. setCoverBlur(image);
  4494. });
  4495. }
  4496.  
  4497. async function decideAnimeCover(response) {
  4498. const badCovers = getBadCovers();
  4499. const candidates = [];
  4500. let results = [];
  4501. try {
  4502. results = JSON.parse(response).items;
  4503. }
  4504. catch (e) {
  4505. return;
  4506. }
  4507. if (results === undefined) {
  4508. $('#anitracker-cover-spinner').remove();
  4509. return;
  4510. }
  4511. for (const result of results) {
  4512. let imgUrl = result['pagemap']?.['metatags']?.[0]?.['og:image'] ||
  4513. result['pagemap']?.['cse_image']?.[0]?.['src'] || result['pagemap']?.['webpage']?.[0]?.['image'] ||
  4514. result['pagemap']?.['metatags']?.[0]?.['twitter:image:src'];
  4515.  
  4516.  
  4517. const width = result['pagemap']?.['cse_thumbnail']?.[0]?.['width'];
  4518. const height = result['pagemap']?.['cse_thumbnail']?.[0]?.['height'];
  4519.  
  4520. if (imgUrl === undefined || height < 100 || badCovers.find(a=> trimHttp(imgUrl).startsWith(trimHttp(a))) !== undefined || imgUrl.endsWith('.gif')) continue;
  4521.  
  4522. if (imgUrl.startsWith('https://static.wikia.nocookie.net')) {
  4523. imgUrl = imgUrl.replace(/\/revision\/latest.*\?cb=\d+$/, '');
  4524. }
  4525.  
  4526. candidates.push({
  4527. src: imgUrl,
  4528. width: width,
  4529. height: height,
  4530. aspectRatio: width / height
  4531. });
  4532. }
  4533.  
  4534. if (candidates.length === 0) return;
  4535.  
  4536. candidates.sort((a, b) => {return a.aspectRatio < b.aspectRatio ? 1 : -1});
  4537.  
  4538. if (candidates[0].src.includes('"')) return;
  4539.  
  4540. const originalBg = $('.anime-cover').css('background-image');
  4541.  
  4542. function badImg() {
  4543. $('.anime-cover').css('background-image', originalBg);
  4544.  
  4545. const storage = getStorage();
  4546. for (const anime of storage.linkList) {
  4547. if (anime.type === 'anime' && anime.animeSession === animeSession) {
  4548. anime.coverImg = /^url\("?([^"]+)"?\)$/.exec(originalBg)[1];
  4549. break;
  4550. }
  4551. }
  4552. saveData(storage);
  4553.  
  4554. $('#anitracker-cover-spinner').remove();
  4555. }
  4556.  
  4557. const image = new Image();
  4558. image.onload = () => {
  4559. if (image.width >= 250) {
  4560.  
  4561. $('.anime-cover').addClass('anitracker-replaced-cover');
  4562. $('.anime-cover').css('background-image', `url("${candidates[0].src}")`);
  4563. $('.anime-cover').attr('image', candidates[0].src);
  4564. setCoverBlur(image);
  4565. const storage = getStorage();
  4566. for (const anime of storage.linkList) {
  4567. if (anime.type === 'anime' && anime.animeSession === animeSession) {
  4568. anime.coverImg = candidates[0].src;
  4569. break;
  4570. }
  4571. }
  4572. saveData(storage);
  4573.  
  4574. $('#anitracker-cover-spinner').remove();
  4575. }
  4576. else badImg();
  4577. };
  4578.  
  4579. image.addEventListener('error', function() {
  4580. badImg();
  4581. });
  4582.  
  4583. image.src = candidates[0].src;
  4584. }
  4585.  
  4586. function hideThumbnails() {
  4587. $('.main').addClass('anitracker-hide-thumbnails');
  4588. }
  4589.  
  4590. function addGeneralButtons() {
  4591. $(`
  4592. <button class="btn btn-dark" id="anitracker-show-data" title="View and handle stored sessions and video progress">
  4593. <i class="fa fa-floppy-o" aria-hidden="true"></i>
  4594. &nbsp;Manage Data...
  4595. </button>
  4596. <button class="btn btn-dark" id="anitracker-settings" title="Settings">
  4597. <i class="fa fa-sliders" aria-hidden="true"></i>
  4598. &nbsp;Settings...
  4599. </button>`).appendTo('#anitracker');
  4600.  
  4601. $('#anitracker-settings').on('click', () => {
  4602. $('#anitracker-modal-body').empty();
  4603. addOptionSwitch('autoplay-video', 'Auto-Play Video', 'Automatically plays the video when it is loaded.', 'autoPlayVideo');
  4604. addOptionSwitch('auto-delete', 'Auto-Clear Links', 'Auto-clearing means only one episode of a series is stored in the tracker at a time.', 'autoDelete');
  4605. addOptionSwitch('theatre-mode', 'Theatre Mode', 'Expand the video player for a better experience on bigger screens.', 'theatreMode');
  4606. addOptionSwitch('hide-thumbnails', 'Hide Thumbnails', 'Hide thumbnails and preview images.', 'hideThumbnails');
  4607. addOptionSwitch('best-quality', 'Default to Best Quality', 'Automatically select the best resolution quality available.', 'bestQuality');
  4608. addOptionSwitch('auto-download', 'Automatic Download', 'Automatically download the episode when visiting a download page.', 'autoDownload');
  4609.  
  4610. if (isEpisode()) {
  4611. $(`
  4612. <div class="btn-group">
  4613. <button class="btn btn-secondary" id="anitracker-reset-player" title="Reset the video player">
  4614. <i class="fa fa-rotate-right" aria-hidden="true"></i>
  4615. &nbsp;Reset player
  4616. </button></div>`).appendTo('#anitracker-modal-body');
  4617.  
  4618. $('#anitracker-reset-player').on('click', function() {
  4619. closeModal();
  4620. setIframeUrl(stripUrl($('.embed-responsive-item').attr('src')));
  4621. });
  4622. }
  4623.  
  4624. openModal();
  4625. });
  4626.  
  4627. function openShowDataModal() {
  4628. $('#anitracker-modal-body').empty();
  4629. $(`
  4630. <div class="anitracker-modal-list-container">
  4631. <div class="anitracker-storage-data" tabindex="0" key="linkList">
  4632. <span>Session Data</span>
  4633. </div>
  4634. </div>
  4635. <div class="anitracker-modal-list-container">
  4636. <div class="anitracker-storage-data" tabindex="0" key="videoTimes">
  4637. <span>Video Progress</span>
  4638. </div>
  4639. </div>
  4640. <div class="btn-group">
  4641. <button class="btn btn-danger" id="anitracker-reset-data" title="Remove stored data and reset all settings">
  4642. <i class="fa fa-undo" aria-hidden="true"></i>
  4643. &nbsp;Reset Data
  4644. </button>
  4645. </div>
  4646. <div class="btn-group">
  4647. <button class="btn btn-secondary" id="anitracker-raw-data" title="View data in JSON format">
  4648. <i class="fa fa-code" aria-hidden="true"></i>
  4649. &nbsp;Raw
  4650. </button>
  4651. </div>
  4652. <div class="btn-group">
  4653. <button class="btn btn-secondary" id="anitracker-export-data" title="Export and download the JSON data">
  4654. <i class="fa fa-download" aria-hidden="true"></i>
  4655. &nbsp;Export Data
  4656. </button>
  4657. </div>
  4658. <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.">
  4659. <i class="fa fa-upload" aria-hidden="true"></i>
  4660. &nbsp;Import Data
  4661. </label>
  4662. <div class="btn-group">
  4663. <button class="btn btn-dark" id="anitracker-edit-data" title="Edit a key">
  4664. <i class="fa fa-pencil" aria-hidden="true"></i>
  4665. &nbsp;Edit...
  4666. </button>
  4667. </div>
  4668. <input type="file" id="anitracker-import-data" style="visibility: hidden; width: 0;" accept=".json">
  4669. `).appendTo('#anitracker-modal-body');
  4670.  
  4671. const expandIcon = `<i class="fa fa-plus anitracker-expand-data-icon" aria-hidden="true"></i>`;
  4672. const contractIcon = `<i class="fa fa-minus anitracker-expand-data-icon" aria-hidden="true"></i>`;
  4673.  
  4674. $(expandIcon).appendTo('.anitracker-storage-data');
  4675.  
  4676. $('.anitracker-storage-data').on('click keydown', (e) => {
  4677. if (e.type === 'keydown' && e.key !== "Enter") return;
  4678. toggleExpandData($(e.currentTarget));
  4679. });
  4680.  
  4681. function toggleExpandData(elem) {
  4682. if (elem.hasClass('anitracker-expanded')) {
  4683. contractData(elem);
  4684. }
  4685. else {
  4686. expandData(elem);
  4687. }
  4688. }
  4689.  
  4690. $('#anitracker-reset-data').on('click', function() {
  4691. if (confirm('[AnimePahe Improvements]\n\nThis will remove all saved data and reset it to its default state.\nAre you sure?') === true) {
  4692. saveData(getDefaultData());
  4693. openShowDataModal();
  4694. }
  4695. });
  4696.  
  4697. $('#anitracker-raw-data').on('click', function() {
  4698. const blob = new Blob([JSON.stringify(getStorage())], {type : 'application/json'});
  4699. windowOpen(URL.createObjectURL(blob));
  4700. });
  4701.  
  4702. $('#anitracker-edit-data').on('click', function() {
  4703. $('#anitracker-modal-body').empty();
  4704. $(`
  4705. <b>Warning: for developer use.<br>Back up your data before messing with this.</b>
  4706. <input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-edit-data-key" placeholder="Key (Path)">
  4707. <input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-edit-data-value" placeholder="Value (JSON)">
  4708. <p>Leave value empty to get the existing value</p>
  4709. <div class="btn-group">
  4710. <button class="btn dropdown-toggle btn-secondary anitracker-edit-mode-dropdown-button" data-bs-toggle="dropdown" data-toggle="dropdown" data-value="replace">Replace</button>
  4711. <div class="dropdown-menu anitracker-dropdown-content anitracker-edit-mode-dropdown"></div>
  4712. </div>
  4713. <div class="btn-group">
  4714. <button class="btn btn-primary anitracker-confirm-edit-button">Confirm</button>
  4715. </div>
  4716. `).appendTo('#anitracker-modal-body');
  4717.  
  4718. [{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') });
  4719.  
  4720. $('.anitracker-edit-mode-dropdown button').on('click', (e) => {
  4721. const pressed = $(e.target)
  4722. const btn = pressed.parents().eq(1).find('.anitracker-edit-mode-dropdown-button');
  4723. btn.data('value', pressed.attr('ref'));
  4724. btn.text(pressed.text());
  4725. });
  4726.  
  4727. $('.anitracker-confirm-edit-button').on('click', () => {
  4728. const storage = getStorage();
  4729. const key = $('.anitracker-edit-data-key').val();
  4730. let keyValue = undefined;
  4731. try {
  4732. keyValue = eval("storage." + key); // lots of evals here because I'm lazy
  4733. }
  4734. catch (e) {
  4735. console.error(e);
  4736. alert("Nope didn't work");
  4737. return;
  4738. }
  4739.  
  4740. if ($('.anitracker-edit-data-value').val() === '') {
  4741. alert(JSON.stringify(keyValue));
  4742. return;
  4743. }
  4744.  
  4745. if (keyValue === undefined) {
  4746. alert("Undefined");
  4747. return;
  4748. }
  4749.  
  4750. const mode = $('.anitracker-edit-mode-dropdown-button').data('value');
  4751.  
  4752. let value = undefined;
  4753. if (mode === 'delList') {
  4754. value = $('.anitracker-edit-data-value').val();
  4755. }
  4756. else if ($('.anitracker-edit-data-value').val() !== "undefined") {
  4757. try {
  4758. value = JSON.parse($('.anitracker-edit-data-value').val());
  4759. }
  4760. catch (e) {
  4761. console.error(e);
  4762. alert("Invalid JSON");
  4763. return;
  4764. }
  4765. }
  4766.  
  4767. 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.";
  4768.  
  4769. switch (mode) {
  4770. case 'replace':
  4771. eval(`storage.${key} = value`);
  4772. break;
  4773. case 'append':
  4774. if (keyValue.constructor.name !== 'Array') {
  4775. alert("Not a list");
  4776. return;
  4777. }
  4778. eval(`storage.${key}.push(value)`);
  4779. break;
  4780. case 'delList':
  4781. if (keyValue.constructor.name !== 'Array') {
  4782. alert("Not a list");
  4783. return;
  4784. }
  4785. try {
  4786. eval(`storage.${key} = storage.${key}.filter(a => !(${value}))`);
  4787. }
  4788. catch (e) {
  4789. console.error(e);
  4790. alert(delFromListMessage);
  4791. return;
  4792. }
  4793. break;
  4794. default:
  4795. alert("This message isn't supposed to show up. Uh...");
  4796. return;
  4797. }
  4798. if (JSON.stringify(storage) === JSON.stringify(getStorage())) {
  4799. alert("Nothing changed.");
  4800. if (mode === 'delList') {
  4801. alert(delFromListMessage);
  4802. }
  4803. return;
  4804. }
  4805. else alert("Probably worked!");
  4806.  
  4807. saveData(storage);
  4808. });
  4809.  
  4810. openModal(openShowDataModal);
  4811. });
  4812.  
  4813. $('#anitracker-export-data').on('click', function() {
  4814. const storage = getStorage();
  4815.  
  4816. if (storage.cache) {
  4817. delete storage.cache;
  4818. saveData(storage);
  4819. }
  4820. download('animepahe-tracked-data-' + Date.now() + '.json', JSON.stringify(getStorage(), null, 2));
  4821. });
  4822.  
  4823. $('#anitracker-import-data-label').on('keydown', (e) => {
  4824. if (e.key === "Enter") $("#" + $(e.currentTarget).attr('for')).click();
  4825. });
  4826.  
  4827. $('#anitracker-import-data').on('change', function(event) {
  4828. const file = this.files[0];
  4829. const fileReader = new FileReader();
  4830. $(fileReader).on('load', function() {
  4831. let newData = {};
  4832. try {
  4833. newData = JSON.parse(fileReader.result);
  4834. }
  4835. catch (err) {
  4836. alert('[AnimePahe Improvements]\n\nPlease input a valid JSON file.');
  4837. return;
  4838. }
  4839.  
  4840. const storage = getStorage();
  4841. const diffBefore = importData(storage, newData, false);
  4842.  
  4843. let totalChanged = 0;
  4844. for (const [key, value] of Object.entries(diffBefore)) {
  4845. totalChanged += value;
  4846. }
  4847.  
  4848. if (totalChanged === 0) {
  4849. alert('[AnimePahe Improvements]\n\nThis file contains no changes to import.');
  4850. return;
  4851. }
  4852.  
  4853. $('#anitracker-modal-body').empty();
  4854.  
  4855. $(`
  4856. <h4>Choose what to import</h4>
  4857. <br>
  4858. <div class="form-check">
  4859. <input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-link-list-check" ${diffBefore.linkListAdded > 0 ? "checked" : "disabled"}>
  4860. <label class="form-check-label" for="anitracker-link-list-check">
  4861. Session entries (${diffBefore.linkListAdded})
  4862. </label>
  4863. </div>
  4864. <div class="form-check">
  4865. <input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-video-times-check" ${(diffBefore.videoTimesAdded + diffBefore.videoTimesUpdated) > 0 ? "checked" : "disabled"}>
  4866. <label class="form-check-label" for="anitracker-video-times-check">
  4867. Video progress times (${diffBefore.videoTimesAdded + diffBefore.videoTimesUpdated})
  4868. </label>
  4869. </div>
  4870. <div class="form-check">
  4871. <input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-bookmarks-check" ${diffBefore.bookmarksAdded > 0 ? "checked" : "disabled"}>
  4872. <label class="form-check-label" for="anitracker-bookmarks-check">
  4873. Bookmarks (${diffBefore.bookmarksAdded})
  4874. </label>
  4875. </div>
  4876. <div class="form-check">
  4877. <input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-notifications-check" ${(diffBefore.notificationsAdded + diffBefore.episodeFeedUpdated) > 0 ? "checked" : "disabled"}>
  4878. <label class="form-check-label" for="anitracker-notifications-check">
  4879. Episode feed entries (${diffBefore.notificationsAdded})
  4880. <ul style="margin-bottom:0;margin-left:-24px;"><li>Episode feed entries updated: ${diffBefore.episodeFeedUpdated}</li></ul>
  4881. </label>
  4882. </div>
  4883. <div class="form-check">
  4884. <input class="form-check-input anitracker-import-data-input" type="checkbox" value="" id="anitracker-settings-check" ${diffBefore.settingsUpdated > 0 ? "checked" : "disabled"}>
  4885. <label class="form-check-label" for="anitracker-settings-check">
  4886. Settings (${diffBefore.settingsUpdated})
  4887. </label>
  4888. </div>
  4889. <div class="btn-group" style="float: right;">
  4890. <button class="btn btn-primary" id="anitracker-confirm-import" title="Confirm import">
  4891. <i class="fa fa-upload" aria-hidden="true"></i>
  4892. &nbsp;Import
  4893. </button>
  4894. </div>
  4895. `).appendTo('#anitracker-modal-body');
  4896.  
  4897. $('.anitracker-import-data-input').on('change', (e) => {
  4898. let checksOn = 0;
  4899. for (const elem of $('.anitracker-import-data-input')) {
  4900. if ($(elem).prop('checked')) checksOn++;
  4901. }
  4902. if (checksOn === 0) {
  4903. $('#anitracker-confirm-import').attr('disabled', true);
  4904. }
  4905. else {
  4906. $('#anitracker-confirm-import').attr('disabled', false);
  4907. }
  4908. });
  4909.  
  4910. $('#anitracker-confirm-import').on('click', () => {
  4911. const diffAfter = importData(getStorage(), newData, true, {
  4912. linkList: !$('#anitracker-link-list-check').prop('checked'),
  4913. videoTimes: !$('#anitracker-video-times-check').prop('checked'),
  4914. bookmarks: !$('#anitracker-bookmarks-check').prop('checked'),
  4915. notifications: !$('#anitracker-notifications-check').prop('checked'),
  4916. settings: !$('#anitracker-settings-check').prop('checked')
  4917. });
  4918.  
  4919. if ((diffAfter.bookmarksAdded + diffAfter.notificationsAdded + diffAfter.settingsUpdated) > 0) updatePage();
  4920. if ((diffAfter.videoTimesUpdated + diffAfter.videoTimesAdded) > 0 && isEpisode()) {
  4921. sendMessage({action:"change_time", time:getStorage().videoTimes.find(a => a.videoUrls.includes($('.embed-responsive-item')[0].src))?.time});
  4922. }
  4923. alert('[AnimePahe Improvements]\n\nImported!');
  4924. openShowDataModal();
  4925. });
  4926.  
  4927. openModal(openShowDataModal);
  4928. });
  4929. fileReader.readAsText(file);
  4930. });
  4931.  
  4932. function importData(data, importedData, save = true, ignored = {settings:{}}) {
  4933. const changed = {
  4934. linkListAdded: 0, // Session entries added
  4935. videoTimesAdded: 0, // Video progress entries added
  4936. videoTimesUpdated: 0, // Video progress times updated
  4937. bookmarksAdded: 0, // Bookmarks added
  4938. notificationsAdded: 0, // Anime added to episode feed
  4939. episodeFeedUpdated: 0, // Episodes either added to episode feed or that had their watched status updated
  4940. settingsUpdated: 0 // Settings updated
  4941. }
  4942.  
  4943. for (const [key, value] of Object.entries(importedData)) {
  4944. if (getDefaultData()[key] === undefined || ignored.settings[key]) continue;
  4945.  
  4946. if (!ignored.linkList && key === 'linkList') {
  4947. const added = [];
  4948. value.forEach(g => {
  4949. if ((g.type === 'episode' && data.linkList.find(h => h.type === 'episode' && h.animeSession === g.animeSession && h.episodeSession === g.episodeSession) === undefined)
  4950. || (g.type === 'anime' && data.linkList.find(h => h.type === 'anime' && h.animeSession === g.animeSession) === undefined)) {
  4951. added.push(g);
  4952. changed.linkListAdded++;
  4953. }
  4954. });
  4955. data.linkList.splice(0,0,...added);
  4956. continue;
  4957. }
  4958. else if (!ignored.videoTimes && key === 'videoTimes') {
  4959. const added = [];
  4960. value.forEach(g => {
  4961. const foundTime = data.videoTimes.find(h => h.videoUrls.includes(g.videoUrls[0]));
  4962. if (foundTime === undefined) {
  4963. added.push(g);
  4964. changed.videoTimesAdded++;
  4965. }
  4966. else if (foundTime.time < g.time) {
  4967. foundTime.time = g.time;
  4968. changed.videoTimesUpdated++;
  4969. }
  4970. });
  4971. data.videoTimes.splice(0,0,...added);
  4972. continue;
  4973. }
  4974. else if (!ignored.bookmarks && key === 'bookmarks') {
  4975. value.forEach(g => {
  4976. if (data.bookmarks.find(h => h.id === g.id) !== undefined) return;
  4977. data.bookmarks.push(g);
  4978. changed.bookmarksAdded++;
  4979. });
  4980. continue;
  4981. }
  4982. else if (!ignored.notifications && key === 'notifications') {
  4983. value.anime.forEach(g => {
  4984. if (data.notifications.anime.find(h => h.id === g.id) !== undefined) return;
  4985. data.notifications.anime.push(g);
  4986. changed.notificationsAdded++;
  4987. });
  4988.  
  4989. // Checking if there exists any gap between the imported episodes and the existing ones
  4990. if (save) data.notifications.anime.forEach(g => {
  4991. const existingEpisodes = data.notifications.episodes.filter(a => (a.animeName === g.name || a.animeId === g.id));
  4992. const addedEpisodes = value.episodes.filter(a => (a.animeName === g.name || a.animeId === g.id));
  4993. if (existingEpisodes.length > 0 && (existingEpisodes[existingEpisodes.length-1].episode - addedEpisodes[0].episode > 1.5)) {
  4994. g.updateFrom = new Date(addedEpisodes[0].time + " UTC").getTime();
  4995. }
  4996. });
  4997.  
  4998. value.episodes.forEach(g => {
  4999. const anime = (() => {
  5000. if (g.animeId !== undefined) return data.notifications.anime.find(a => a.id === g.animeId);
  5001.  
  5002. const fromNew = data.notifications.anime.find(a => a.name === g.animeName);
  5003. if (fromNew !== undefined) return fromNew;
  5004. const id = value.anime.find(a => a.name === g.animeName);
  5005. return data.notifications.anime.find(a => a.id === id);
  5006. })();
  5007. if (anime === undefined) return;
  5008. if (g.animeName !== anime.name) g.animeName = anime.name;
  5009. if (g.animeId === undefined) g.animeId = anime.id;
  5010. const foundEpisode = data.notifications.episodes.find(h => h.animeId === anime.id && h.episode === g.episode);
  5011. if (foundEpisode !== undefined) {
  5012. if (g.watched === true && !foundEpisode.watched) {
  5013. foundEpisode.watched = true;
  5014. changed.episodeFeedUpdated++;
  5015. }
  5016. return;
  5017. }
  5018. data.notifications.episodes.push(g);
  5019. changed.episodeFeedUpdated++;
  5020. });
  5021. if (save) {
  5022. data.notifications.episodes.sort((a,b) => a.time < b.time ? 1 : -1);
  5023. if (value.episodes.length > 0) {
  5024. data.notifications.lastUpdated = new Date(value.episodes[0].time + " UTC").getTime();
  5025. }
  5026. }
  5027. continue;
  5028. }
  5029. if ((value !== true && value !== false) || data[key] === undefined || data[key] === value || ignored.settings === true) continue;
  5030. data[key] = value;
  5031. changed.settingsUpdated++;
  5032. }
  5033.  
  5034. if (save) saveData(data);
  5035.  
  5036. return changed;
  5037. }
  5038.  
  5039. function getCleanType(type) {
  5040. if (type === 'linkList') return "Clean up older duplicate entries";
  5041. else if (type === 'videoTimes') return "Remove entries with no progress (0s)";
  5042. else return "[Message not found]";
  5043. }
  5044.  
  5045. function expandData(elem) {
  5046. const storage = getStorage();
  5047. const dataType = elem.attr('key');
  5048.  
  5049. elem.find('.anitracker-expand-data-icon').replaceWith(contractIcon);
  5050. const dataEntries = $('<div class="anitracker-modal-list"></div>').appendTo(elem.parent());
  5051.  
  5052. $(`
  5053. <div class="btn-group anitracker-storage-filter">
  5054. <input autocomplete="off" class="form-control anitracker-text-input-bar anitracker-modal-search" placeholder="Search">
  5055. <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>
  5056. <button class="btn btn-secondary anitracker-clean-data-button anitracker-list-btn" style="text-wrap:nowrap;" title="${getCleanType(dataType)}">Clean up</button>
  5057. </div>
  5058. `).appendTo(dataEntries);
  5059. elem.parent().find('.anitracker-modal-search').focus();
  5060.  
  5061. elem.parent().find('.anitracker-modal-search').on('input', (e) => {
  5062. setTimeout(() => {
  5063. const query = $(e.target).val();
  5064. for (const entry of elem.parent().find('.anitracker-modal-list-entry')) {
  5065. if ($($(entry).find('a,span')[0]).text().toLowerCase().includes(query)) {
  5066. $(entry).show();
  5067. continue;
  5068. }
  5069. $(entry).hide();
  5070. }
  5071. }, 10);
  5072. });
  5073.  
  5074. elem.parent().find('.anitracker-clean-data-button').on('click', () => {
  5075. if (!confirm("[AnimePahe Improvements]\n\n" + getCleanType(dataType) + '?')) return;
  5076.  
  5077. const updatedStorage = getStorage();
  5078.  
  5079. const removed = [];
  5080. if (dataType === 'linkList') {
  5081. for (let i = 0; i < updatedStorage.linkList.length; i++) {
  5082. const link = updatedStorage.linkList[i];
  5083.  
  5084. const similar = updatedStorage.linkList.filter(a => a.animeName === link.animeName && a.episodeNum === link.episodeNum);
  5085. if (similar[similar.length-1] !== link) {
  5086. removed.push(link);
  5087. }
  5088. }
  5089. updatedStorage.linkList = updatedStorage.linkList.filter(a => !removed.includes(a));
  5090. }
  5091. else if (dataType === 'videoTimes') {
  5092. for (const timeEntry of updatedStorage.videoTimes) {
  5093. if (timeEntry.time > 5) continue;
  5094. removed.push(timeEntry);
  5095. }
  5096. updatedStorage.videoTimes = updatedStorage.videoTimes.filter(a => !removed.includes(a));
  5097. }
  5098.  
  5099. alert(`[AnimePahe Improvements]\n\nCleaned up ${removed.length} ${removed.length === 1 ? "entry" : "entries"}.`);
  5100.  
  5101. saveData(updatedStorage);
  5102. dataEntries.remove();
  5103. expandData(elem);
  5104. });
  5105.  
  5106. // When clicking the reverse order button
  5107. elem.parent().find('.anitracker-reverse-order-button').on('click', (e) => {
  5108. const btn = $(e.target);
  5109. if (btn.attr('dir') === 'down') {
  5110. btn.attr('dir', 'up');
  5111. btn.addClass('anitracker-up');
  5112. }
  5113. else {
  5114. btn.attr('dir', 'down');
  5115. btn.removeClass('anitracker-up');
  5116. }
  5117.  
  5118. const entries = [];
  5119. for (const entry of elem.parent().find('.anitracker-modal-list-entry')) {
  5120. entries.push(entry.outerHTML);
  5121. }
  5122. entries.reverse();
  5123. elem.parent().find('.anitracker-modal-list-entry').remove();
  5124. for (const entry of entries) {
  5125. $(entry).appendTo(elem.parent().find('.anitracker-modal-list'));
  5126. }
  5127. applyDeleteEvents();
  5128. });
  5129.  
  5130. function applyDeleteEvents() {
  5131. $('.anitracker-modal-list-entry .anitracker-delete-session-button').on('click', function() {
  5132. const storage = getStorage();
  5133.  
  5134. const href = $(this).parent().find('a').attr('href');
  5135. const animeSession = getAnimeSessionFromUrl(href);
  5136.  
  5137. if (isEpisode(href)) {
  5138. const episodeSession = getEpisodeSessionFromUrl(href);
  5139. storage.linkList = storage.linkList.filter(g => !(g.type === 'episode' && g.animeSession === animeSession && g.episodeSession === episodeSession));
  5140. saveData(storage);
  5141. }
  5142. else {
  5143. storage.linkList = storage.linkList.filter(g => !(g.type === 'anime' && g.animeSession === animeSession));
  5144. saveData(storage);
  5145. }
  5146.  
  5147. $(this).parent().remove();
  5148. });
  5149.  
  5150. $('.anitracker-modal-list-entry .anitracker-delete-progress-button').on('click', function() {
  5151. const storage = getStorage();
  5152. storage.videoTimes = storage.videoTimes.filter(g => !g.videoUrls.includes($(this).attr('lookForUrl')));
  5153. saveData(storage);
  5154.  
  5155. $(this).parent().remove();
  5156. });
  5157. }
  5158.  
  5159. if (dataType === 'linkList') {
  5160. [...storage.linkList].reverse().forEach(g => {
  5161. const name = g.animeName + (g.type === 'episode' ? (' - Episode ' + g.episodeNum) : '');
  5162. $(`
  5163. <div class="anitracker-modal-list-entry">
  5164. <a target="_blank" href="/${(g.type === 'episode' ? 'play/' : 'anime/') + g.animeSession + (g.type === 'episode' ? ('/' + g.episodeSession) : '')}" title="${toHtmlCodes(name)}">
  5165. ${name}
  5166. </a><br>
  5167. <button class="btn btn-danger anitracker-delete-session-button" title="Delete this stored session">
  5168. <i class="fa fa-trash" aria-hidden="true"></i>
  5169. &nbsp;Delete
  5170. </button>
  5171. </div>`).appendTo(elem.parent().find('.anitracker-modal-list'));
  5172. });
  5173.  
  5174. applyDeleteEvents();
  5175. }
  5176. else if (dataType === 'videoTimes') {
  5177. [...storage.videoTimes].reverse().forEach(g => {
  5178. $(`
  5179. <div class="anitracker-modal-list-entry">
  5180. <span>
  5181. ${g.animeName} - Episode ${g.episodeNum}
  5182. </span><br>
  5183. <span>
  5184. Current time: ${secondsToHMS(g.time)}
  5185. </span><br>
  5186. <button class="btn btn-danger anitracker-delete-progress-button" lookForUrl="${g.videoUrls[0]}" title="Delete this video progress">
  5187. <i class="fa fa-trash" aria-hidden="true"></i>
  5188. &nbsp;Delete
  5189. </button>
  5190. </div>`).appendTo(elem.parent().find('.anitracker-modal-list'));
  5191. });
  5192.  
  5193. applyDeleteEvents();
  5194. }
  5195.  
  5196. elem.addClass('anitracker-expanded');
  5197. }
  5198.  
  5199. function contractData(elem) {
  5200. elem.find('.anitracker-expand-data-icon').replaceWith(expandIcon);
  5201.  
  5202. elem.parent().find('.anitracker-modal-list').remove();
  5203.  
  5204. elem.removeClass('anitracker-expanded');
  5205. elem.blur();
  5206. }
  5207.  
  5208. openModal();
  5209. }
  5210.  
  5211. $('#anitracker-show-data').on('click', openShowDataModal);
  5212. }
  5213.  
  5214. addGeneralButtons();
  5215. if (isEpisode()) {
  5216. $(`
  5217. <span style="margin-left: 30px;"><i class="fa fa-files-o" aria-hidden="true"></i>&nbsp;Copy:</span>
  5218. <div class="btn-group">
  5219. <button class="btn btn-dark anitracker-copy-button" copy="link" data-placement="top" data-content="Copied!">Link</button>
  5220. </div>
  5221. <div class="btn-group" style="margin-right:30px;">
  5222. <button class="btn btn-dark anitracker-copy-button" copy="link-time" data-placement="top" data-content="Copied!">Link & Time</button>
  5223. </div>`).appendTo('#anitracker');
  5224. addOptionSwitch('autoplay-next','Auto-Play Next','Automatically go to the next episode when the current one has ended.','autoPlayNext','#anitracker');
  5225.  
  5226. $('.anitracker-copy-button').on('click', (e) => {
  5227. const targ = $(e.currentTarget);
  5228. const type = targ.attr('copy');
  5229. const name = encodeURIComponent(getAnimeName());
  5230. const episode = getEpisodeNum();
  5231. if (['link','link-time'].includes(type)) {
  5232. navigator.clipboard.writeText(window.location.origin + '/customlink?a=' + name + '&e=' + episode + (type !== 'link-time' ? '' : ('&t=' + currentEpisodeTime.toString())));
  5233. }
  5234. targ.popover('show');
  5235. setTimeout(() => {
  5236. targ.popover('hide');
  5237. }, 1000);
  5238. });
  5239. }
  5240.  
  5241. if (initialStorage.autoDelete === true && isEpisode() && paramArray.find(a => a[0] === 'ref' && a[1] === 'customlink') === undefined) {
  5242. const animeData = getAnimeData();
  5243. deleteEpisodesFromTracker(getEpisodeNum(), animeData.title, animeData.id);
  5244. }
  5245.  
  5246. function updateSwitches() {
  5247. const storage = getStorage();
  5248.  
  5249. for (const s of optionSwitches) {
  5250. if (s.value !== storage[s.optionId]) {
  5251. s.value = storage[s.optionId];
  5252. }
  5253. if (s.value === true) {
  5254. if (s.onEvent !== undefined) s.onEvent();
  5255. }
  5256. else if (s.offEvent !== undefined) {
  5257. s.offEvent();
  5258. }
  5259. }
  5260.  
  5261. optionSwitches.forEach(s => {
  5262. $(`#anitracker-${s.switchId}-switch`).prop('checked', storage[s.optionId] === true);
  5263. $(`#anitracker-${s.switchId}-switch`).change();
  5264. });
  5265. }
  5266.  
  5267. updateSwitches();
  5268.  
  5269. function addOptionSwitch(id, name, desc = '', optionId, parent = '#anitracker-modal-body') {
  5270. const option = optionSwitches.find(s => s.optionId === optionId);
  5271.  
  5272. $(`
  5273. <div class="custom-control custom-switch anitracker-switch" id="anitracker-${id}" title="${desc}">
  5274. <input type="checkbox" class="custom-control-input" id="anitracker-${id}-switch">
  5275. <label class="custom-control-label" for="anitracker-${id}-switch">${name}</label>
  5276. </div>`).appendTo(parent);
  5277. const switc = $(`#anitracker-${id}-switch`);
  5278. switc.prop('checked', option.value);
  5279.  
  5280. const events = [option.onEvent, option.offEvent];
  5281.  
  5282. switc.on('change', (e) => {
  5283. const checked = $(e.currentTarget).is(':checked');
  5284. const storage = getStorage();
  5285.  
  5286. if (checked !== storage[optionId]) {
  5287. storage[optionId] = checked;
  5288. option.value = checked;
  5289. saveData(storage);
  5290. }
  5291.  
  5292. if (checked) {
  5293. if (events[0] !== undefined) events[0]();
  5294. }
  5295. else if (events[1] !== undefined) events[1]();
  5296. });
  5297. }
  5298.  
  5299. $(`
  5300. <div class="anitracker-download-spinner" style="display: none;">
  5301. <div class="spinner-border text-danger" role="status">
  5302. <span class="sr-only">Loading...</span>
  5303. </div>
  5304. </div>`).prependTo('#downloadMenu,#episodeMenu');
  5305. $('.prequel img,.sequel img').attr('loading','');
  5306. }