AnimePahe Improvements

Improvements and additions for the AnimePahe site

当前为 2025-01-09 提交的版本,查看 最新版本

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