AnimePahe Improvements

Improvements and additions for the AnimePahe site

目前为 2024-12-19 提交的版本,查看 最新版本

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