Mangago Backup Lists

Backup your reading lists

  1. // ==UserScript==
  2. // @name Mangago Backup Lists
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2.4
  5. // @description Backup your reading lists
  6. // @author You
  7. // @match https://www.mangago.me/*
  8. // @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery.isotope/3.0.6/isotope.pkgd.min.js
  10. // @run-at document-start
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // plugins used:
  18. // Isotope - for sortable list view: https://isotope.metafizzy.co/
  19. // lz-string - for string compression: https://pieroxy.net/blog/pages/lz-string/index.html
  20. console.log('mggBackup v.1.2.4');
  21.  
  22. // Current limit for sortable view. Any more than this results in degraded browser performance.
  23. // Sadly, this is probably the limit of what I can do with this userscript on mgg's site + given its current structure and capability.
  24. // I'm working on getting a separate website up and running so that we can have a more.
  25. // For actual backups (.csv(excel)/.json) though, I've tested it working okay for up to 10,000 stories. Beyond that, I'm afraid I can't guarantee success :'<
  26. const idealLimit = 1000
  27.  
  28. // function that detects how many current pages a specific list has
  29. function setTotalPages() {
  30. const $paginagtion = $('.pagination').first();
  31. let totalPages = undefined
  32.  
  33. if ($paginagtion[0]) {
  34. totalPages = $paginagtion.attr('total');
  35. } else {
  36. totalPages = 1;
  37. }
  38.  
  39. localStorage.setItem('totalPages', totalPages);
  40. }
  41.  
  42. function detectList() {
  43. // detects list by current URL
  44. if (getUrlWithoutParams().match(/(manga\/1\/)/gm)) {
  45. return 1 // manga/1/ = Want To Read
  46. } else if ((getUrlWithoutParams().match(/(manga\/2\/)/gm))) {
  47. return 2 //manga/2/ = Currently Reading
  48. } else if (getUrlWithoutParams().match(/(manga\/3\/)/gm)) {
  49. return 3 //manga/3/ = Already Read
  50. }
  51.  
  52. return 0
  53. }
  54.  
  55. // function that gathers details about the stories to save
  56. function saveList(custom = false) {
  57. console.log('saving...');
  58.  
  59. if (!custom) {
  60. const arr = [];
  61. const $toSave = $('#collection-nav').next().find('.manga');
  62.  
  63. $toSave.each(function() {
  64. const $this = $(this);
  65.  
  66. // gets details for each story
  67. const $titleLink = $this.find('.title').find('a');
  68. const title = $titleLink.text();
  69.  
  70. const author = $this.find('.title').next().find('div').text().split("|")[1].trim();
  71.  
  72. const link = $titleLink.attr('href');
  73. const cover = $this.find('.cover').find('img').data('src');
  74. let timestamp = '';
  75. let tags = [];
  76.  
  77. $this.find('.status-rate').find('div').each(function() {
  78. if ($(this).text().match(/(marked)/gm)) {
  79. timestamp = $(this).text().replace(/(marked)/gm, '').trim();
  80. }
  81.  
  82. if ($(this).text().match(/(Tags)/gm)) {
  83. $(this).find('.tag')?.each(function() {
  84. tags.push($(this).text().trim());
  85. });
  86. }
  87. })
  88.  
  89. const $note = $this.find('.short-note');
  90.  
  91. const note = {
  92. text: $note.text().trim(),
  93. html: $note.text().trim() ? $note[0].outerHTML : ``
  94. }
  95.  
  96. let rating = undefined
  97.  
  98. const $stars = $this.find('.status-rate').find('.stars9').first().find('.stars9');
  99.  
  100. let ratingWidth = 0;
  101.  
  102. if ($stars.css('width')) {
  103. ratingWidth = parseInt($stars.css('width').replace(/(px)/gm, ''));
  104. }
  105.  
  106. // star ratings are weird because they are not explicitly exposed as 1,2,3,4,5
  107. // so I'm only getting the width of the yellow fill of the stars and assigning each into 1-5 LMAO
  108. switch (ratingWidth) {
  109. case (11):
  110. rating = 1;
  111. break;
  112. case (22):
  113. rating = 2;
  114. break;
  115. case (33):
  116. rating = 3;
  117. break;
  118. case (44):
  119. rating = 4;
  120. break;
  121. case (55):
  122. rating = 5;
  123. break;
  124. default:
  125. rating = undefined;
  126. break;
  127. }
  128.  
  129. arr.push({
  130. title,
  131. author,
  132. link,
  133. cover,
  134. timestamp,
  135. note,
  136. rating,
  137. tags
  138. });
  139. })
  140.  
  141. // I turned these into single letter keys for space saving.
  142. const listId = detectList() === 1
  143. ? 'w' // want to read
  144. : detectList() === 2
  145. ? 'c' // currently reading
  146. : detectList() === 3
  147. ? 'd' // done reading
  148. : undefined;
  149.  
  150. if (listId) {
  151. // this part is the reason why if you check your localStorage on dev tools, there are weird signs
  152. // I am using lz-string to compress strings to be able to save more stories
  153. const compressed = compressString(JSON.stringify(arr))
  154.  
  155. const { page } = getUrlParams()
  156.  
  157. localStorage.setItem(`${listId + page}`, compressed);
  158. }
  159. } else {
  160. const params = getUrlParams();
  161.  
  162. const { page } = params;
  163.  
  164. const listDetails = {
  165. title: {
  166. text: '',
  167. html: ''
  168. },
  169. listId: '',
  170. curator: {
  171. username: '',
  172. id: '',
  173. },
  174. created: '',
  175. updated: '',
  176. description: {
  177. text: '',
  178. html: ''
  179. },
  180. tags: []
  181. }
  182.  
  183. if (page) {
  184. if (parseInt(page, 10) === 1) {
  185. const $h1 = $('.w-title').find('h1');
  186. const titleText = $h1.text().trim();
  187. listDetails.title.text = titleText;
  188. listDetails.title.html = titleText ? $h1[0].outerHTML : '';
  189. listDetails.listId = getListId();
  190.  
  191. const $userProfile = $('.user-profile')
  192. const $info = $userProfile.find('.info');
  193. const curatorLinkParts = $userProfile.find('.pic').find('a').attr('href').split('/').filter(url => url !== '');
  194. const curatorId = curatorLinkParts[curatorLinkParts.length - 1];
  195. listDetails.curator.username = $info.find('h2').text();
  196. listDetails.curator.id = curatorId;
  197.  
  198. const dates = $info.contents().filter(function(){
  199. return this.nodeType == 3;
  200. })[1].nodeValue.trim().split(': ');
  201.  
  202. listDetails.updated = dates[2];
  203. listDetails.created = dates[1].split('Last')[0].trim();
  204.  
  205. const $description = $('.article').find('.description')
  206.  
  207. const descText = $description.text().trim()
  208.  
  209. listDetails.description.text = descText;
  210. listDetails.description.html = descText ? $description[0].outerHTML : '';
  211.  
  212. const tagsArr = []
  213. const $tags = $('.content').find('.tag');
  214.  
  215. $tags.each((i, el) => {
  216. const $el = $(el);
  217. tagsArr.push($el.text());
  218. })
  219.  
  220. listDetails.tags = tagsArr;
  221.  
  222. const listId = getListId();
  223. const type = getTypeIndexFromCustomList(listId).type;
  224.  
  225. localStorage.setItem(`${type}${listId}-d`, compressString(JSON.stringify(listDetails)));
  226. }
  227. }
  228.  
  229. const arr = [];
  230. const $toSave = $('.manga.note-and-order');
  231.  
  232. const $h1 = $('.w-title').find('h1');
  233. const titleText = $h1.text().trim();
  234. const listTitleDetails = {
  235. text: titleText,
  236. html: titleText ? $h1[0].outerHTML : ''
  237. }
  238.  
  239. $toSave.each(function(i, el) {
  240. const $this = $(el);
  241.  
  242. // gets details for each story
  243. const $titleLink = $this.find('.title').find('a');
  244. const title = $titleLink.text();
  245.  
  246. const author = $this.find('.info').filter((i, el) => {
  247. const $el = $(el);
  248.  
  249. return $el.text().match(/(Author\(s\))/gm);
  250. }).find('span').text();
  251.  
  252.  
  253. const link = $titleLink.attr('href');
  254. const cover = $this.find('.cover').find('img').data('src') || $('.album-photos').find('img').first().attr('src') || 'none';
  255.  
  256. const index = $this.attr('_index');
  257.  
  258. const tags = $this.find('.info').filter((i, el) => {
  259. const $el = $(el);
  260.  
  261. return $el.text().match(/(Genre\(s\))/gm);
  262. }).find('span').text().split('/').map(item => item.trim()).filter(item => item !== "");
  263.  
  264.  
  265. const $note = $this.find('.info.summary');
  266.  
  267. const note = {
  268. text: $note.text().trim(),
  269. html: $note.text().trim() ? $note[0].outerHTML : ``
  270. }
  271.  
  272. const rating = $this.find('.title').next().find('.info').filter((i, el) => {
  273. const $el = $(el);
  274.  
  275. let returnFlag = false;
  276. if ($el.find('#stars0')[0]) {
  277. returnFlag = true;
  278. }
  279.  
  280. return returnFlag;
  281. }).find('span').text().trim();
  282.  
  283. const timestamp = $this.find('.info.summary').next().find('.left').text();
  284.  
  285. arr.push({
  286. title,
  287. author,
  288. link,
  289. cover,
  290. timestamp,
  291. note,
  292. rating,
  293. tags,
  294. index,
  295. listId: getListId(),
  296. listTitleDetails,
  297. });
  298. });
  299.  
  300. const listId = getListId();
  301. const type = getTypeIndexFromCustomList(listId).type;
  302.  
  303. if (listId && type) {
  304. const existing = decompressString(localStorage.getItem(`${type}${listId}`));
  305.  
  306. if (existing) {
  307. const parsedExisting = JSON.parse(existing);
  308. const newArr = [...parsedExisting, ...arr];
  309. const compressedNewArr = compressString(JSON.stringify(newArr));
  310. localStorage.setItem(`${type}${listId}`, compressedNewArr);
  311. } else {
  312. const compressedNewArr = compressString(JSON.stringify(arr));
  313. localStorage.setItem(`${type}${listId}`, compressedNewArr);
  314. }
  315. }
  316. }
  317. }
  318.  
  319. // clear localStorage keys where keys begin with w/c/d (for previously saved stories) from probably previous backups
  320. // x is letter for custom lists, y is for followed lists
  321. function clearListRelatedStorageItems (customList = false) {
  322. const keys = Object.keys(localStorage);
  323.  
  324. let targetArr = [];
  325.  
  326. if (!customList) {
  327. targetArr = ['w', 'c', 'd']
  328. } else {
  329. targetArr = ['x', 'y']
  330. }
  331.  
  332. for ( var i = 0, len = localStorage.length; i < len; ++i ) {
  333. const match = targetArr.indexOf(keys[i].charAt(0)) !== -1;
  334.  
  335. if (match) {
  336. localStorage.removeItem(keys[i]);
  337. }
  338. }
  339. }
  340.  
  341. // other related items saved
  342. function clearLocalStorageItems (specific = []) {
  343. const items = [
  344. 'backupMode', // trigger check for doing page by page backup
  345. 'totalPages', // key for total number of pagination per list type
  346. 'backupTime', // latest available backup time
  347. 'generate', // generate boolean for custom list
  348. ]
  349.  
  350. const finalArrayToTarget = specific.length > 0 ? specific : items
  351.  
  352. finalArrayToTarget.forEach(item => {
  353. localStorage.removeItem(item)
  354. })
  355. }
  356.  
  357. // sortable list will be seen as a tab next to [Done Reading] list and can be triggered when number of stories < idealLimit
  358. function appendSortableList() {
  359. const count = getAllBackup().finalCount;
  360.  
  361. if (!$('#navCustom')[0]) {
  362. $('#collection-nav').append(`
  363. <div id="navCustom" class="nav sub nav-custom">
  364. Sortable List
  365. </div>
  366. `)
  367.  
  368. $('body').append(`<style>
  369. #collection-nav .nav-custom {
  370. background-color: #0069ed;
  371. color: #ffffff;
  372. cursor: pointer;
  373. transition: .2s;
  374. }
  375.  
  376. #collection-nav .nav-custom:hover {
  377. background-color: #ffffff;
  378. color: #0069ed;
  379. cursor: pointer;
  380. }
  381. </style>`)
  382.  
  383. // Clicking the tab redirects to `/manga/4/` an unuse\/4d url, so I just decided to dump backed up stories there
  384. $('#navCustom').on('click', () => {
  385. if (getUserId() !== undefined) {
  386. if (count >= idealLimit) {
  387. const proceed = confirm('Your have more than 1000 stories. The sortable list might be slow, or might not work properly at all. Proceed anyway?')
  388.  
  389. if (proceed) {
  390. window.location.replace(`https://www.mangago.me/home/people/${getUserId()}/manga/4/`);
  391. }
  392. } else {
  393. window.location.replace(`https://www.mangago.me/home/people/${getUserId()}/manga/4/`);
  394. }
  395. } else {
  396. alert('Error: your userId cannot be obtained. Be sure you are on a url where people/1234567/manga... is visible')
  397. }
  398. })
  399. }
  400.  
  401. }
  402.  
  403. // string compression to save space. read more about it over at: https://pieroxy.net/blog/pages/lz-string/index.html
  404. function compressString(string) {
  405. if (LZString) {
  406. return LZString.compressToUTF16(string)
  407. } else {
  408. throw new Error ('lz-string plugin missing')
  409. }
  410. }
  411.  
  412. function decompressString(string) {
  413. if (LZString) {
  414. return LZString.decompressFromUTF16(string);
  415. } else {
  416. throw new Error ('lz-string plugin missing')
  417. }
  418. }
  419.  
  420. // Tags are usually filled with emoticons and special characters. cleaning function for tag identifier when filtering sort view
  421. function cleanAndHyphenateTag(tag) {
  422. return tag.replace(/\W/g, '').replace(/ +/g, '-').toLowerCase();
  423. }
  424.  
  425. // Params are what we see on URLs after the main link. http://sample.com/?paramSample=1
  426. // this can be extracted on the page and this will become a variable named paramSample with a value of 1
  427. function getUrlWithoutParams() {
  428. return window.location.href.split(/[?#]/)[0]
  429. }
  430.  
  431. // mgg currently has use url formatted like this: https://www.mangago.me/home/people/1234567/
  432. // I'm getting the id part with this function
  433. function getUserId() {
  434. const url = window.location.href.split('/')
  435. const isAturlWithUserId = url.some(urlPart => {
  436. return urlPart === "people"
  437. })
  438.  
  439. if (isAturlWithUserId) {
  440. const targetIndex = url.indexOf("people") + 1
  441. return url[targetIndex]
  442. } else {
  443. return undefined
  444. }
  445. }
  446.  
  447. // get Id for custom lists. hereby defining custom lists as any lists that are not want to read/reading/done
  448. function getListId() {
  449. const url = window.location.href.split('/')
  450. const isAturlWithUserId = url.some(urlPart => {
  451. return urlPart === "mangalist"
  452. })
  453.  
  454. if (isAturlWithUserId) {
  455. const targetIndex = url.indexOf("mangalist") + 1
  456. return url[targetIndex]
  457. } else {
  458. return undefined
  459. }
  460. }
  461.  
  462. // detect if part of url has pattern pertaining to custom lists
  463. function isForCustomLists() {
  464. const url = window.location.href.split('/');
  465. const isCustom = url.some(urlPart => urlPart === 'list' || urlPart === 'mangalist');
  466. return isCustom;
  467. }
  468.  
  469. // used when retrieving the stored items into localStorage.
  470. // targets localStorage items that start with w/c/d as set by previous backup
  471. function getListFromStorage(letterId, custom = false, fetchMainDetails = false) {
  472. const keys = Object.keys(localStorage);
  473.  
  474. let finalKeys = keys.filter((key) => {
  475. if (custom) {
  476. if (fetchMainDetails) {
  477. return key.charAt(0) === letterId && key.match(/(-d)/gm)
  478. } else {
  479. return key.charAt(0) === letterId && !key.match(/(-d)/gm)
  480. }
  481. }
  482.  
  483. return key.charAt(0) === letterId
  484. })
  485.  
  486. const finalArr = []
  487.  
  488. finalKeys.forEach(key => {
  489. finalArr.push(JSON.parse(decompressString(localStorage.getItem(key))))
  490. })
  491.  
  492. return finalArr.flat()
  493. }
  494.  
  495. // compiles all lists from storage
  496. function getAllBackup() {
  497. const wantToRead = getListFromStorage('w');
  498. const currentlyReading = getListFromStorage('c');
  499. const alreadyRead = getListFromStorage('d');
  500.  
  501. const finalObj = {
  502. wantToRead,
  503. currentlyReading,
  504. alreadyRead
  505. }
  506.  
  507. const allData = [
  508. {
  509. arr: wantToRead,
  510. id: 'wantToRead'
  511. },
  512. {
  513. arr: currentlyReading,
  514. id: 'currentlyReading'
  515. },
  516. {
  517. arr: alreadyRead,
  518. id: 'alreadyRead'
  519. },
  520. ]
  521.  
  522. return {
  523. allData,
  524. finalObj,
  525. finalCount: wantToRead.length + currentlyReading.length + alreadyRead.length
  526. }
  527. }
  528.  
  529. function getAllBackupCustom() {
  530. const created = getListFromStorage('x', true);
  531. const followed = getListFromStorage('y', true);
  532. const createdDetails = getListFromStorage('x', true, true);
  533. const followedDetails = getListFromStorage('y', true, true);
  534.  
  535. const finalObj = {
  536. created,
  537. followed,
  538. mainDetails: [...createdDetails, ...followedDetails]
  539. }
  540.  
  541. const allData = [
  542. {
  543. arr: created,
  544. id: 'created'
  545. },
  546. {
  547. arr: followed,
  548. id: 'followed'
  549. },
  550. ]
  551.  
  552. return {
  553. allData,
  554. finalObj,
  555. }
  556. }
  557.  
  558. // function for escaping double quotes (") and commas (,) on tricky CSV formats
  559. // see rules over at https://en.wikipedia.org/wiki/Comma-separated_values#Basic_rules
  560. function escape(value) {
  561. if(!['"','\r','\n',','].some(e => value.indexOf(e) !== -1)) {
  562. return value;
  563. }
  564.  
  565. return '"' + value.replace(/"/g, '""') + '"';
  566. }
  567.  
  568. // attaches all the fetched data into a clickable link that is shaped like a button
  569. // attached near the header on the user profile page when a previous successful backup is available
  570. function generateDownloadLinksToBackup() {
  571. const rows = []
  572.  
  573. const allBackup = getAllBackup()
  574.  
  575. allBackup.allData.forEach(item => {
  576. item.arr.forEach(subitem => {
  577. const { title, author, link, cover, timestamp, note, rating, tags } = subitem
  578. let finalRating = -1
  579.  
  580. if (rating) {
  581. finalRating = rating
  582. }
  583.  
  584. rows.push([
  585. escape(title ? title.toString(): ""),
  586. escape(author ? author.toString(): ""),
  587. escape(link ? link.toString(): ""),
  588. escape(cover ? cover.toString(): ""),
  589. escape(timestamp ? timestamp.toString(): ""),
  590. escape(note.text ? note.text.toString() : ""),
  591. escape(finalRating ? finalRating.toString(): ""),
  592. escape(tags ? tags.join(',').toString(): ""),
  593. escape(item.id ? item.id.toString() : ""),
  594. ])
  595. })
  596. })
  597.  
  598. const latestBackupTime = localStorage.getItem('backupTime');
  599. const fileSafeBackupTime = latestBackupTime.replace(/[^a-z0-9]/gi, '_').toLowerCase();
  600.  
  601. // CSV handling
  602. let csvString = rows.map(e => e.join(",")).join("\n")
  603. let universalBOM = "\uFEFF";
  604. let csvContent = 'data:text/csv; charset=utf-8,' + encodeURIComponent(universalBOM+csvString);
  605.  
  606. let downloadLinkCsv = document.createElement("a");
  607. downloadLinkCsv.href = csvContent;
  608. downloadLinkCsv.download = `list-backup-user${getUserId()}-${fileSafeBackupTime}.csv`;
  609. downloadLinkCsv.classList.add('c-btn');
  610. downloadLinkCsv.classList.add('download-link');
  611. downloadLinkCsv.id = 'downloadCsv';
  612. downloadLinkCsv.text = 'Download CSV';
  613.  
  614. $('.info').find("h1").after(downloadLinkCsv);
  615.  
  616. // JSON data handling
  617. let jsonContent = "data:text/json;charset=utf-8," + "\ufeff" + encodeURIComponent(JSON.stringify(allBackup.finalObj));
  618.  
  619. let downloadLinkJson = document.createElement("a");
  620. downloadLinkJson.href = jsonContent;
  621. downloadLinkJson.download = `list-backup-user${getUserId()}-${fileSafeBackupTime}.json`;
  622. downloadLinkJson.classList.add('c-btn');
  623. downloadLinkJson.classList.add('download-link');
  624. downloadLinkJson.id = 'downloadJson';
  625. downloadLinkJson.text = 'Download JSON';
  626.  
  627. $('.info').find("h1").after(downloadLinkJson);
  628.  
  629. $('.info').find("h1").after(`<span style="display: block; margin-right: 12px; color: #06E8F6; font-size: 16px;">Latest Backup (${latestBackupTime}): </span>`);
  630.  
  631. $('body').append(`<style>
  632. #downloadJson.c-btn {
  633. top: 70px;
  634. }
  635.  
  636. .download-link {
  637. margin-right: 15px;
  638. margin-top: 12px;
  639. }
  640. </style>`)
  641. }
  642.  
  643. // same as above but for custom lists
  644. function generateDownloadLinksToBackupCustom() {
  645. const rows = []
  646.  
  647. const allBackup = getAllBackupCustom()
  648. const finalObj = allBackup.finalObj
  649.  
  650. allBackup.allData.forEach(item => {
  651. item.arr.forEach(subitem => {
  652. const { title, author, link, cover, timestamp, note, rating, tags, index, listId } = subitem;
  653. let finalRating = -1;
  654.  
  655. if (rating) {
  656. finalRating = rating;
  657. }
  658.  
  659. let targetList = finalObj.mainDetails.filter(item => item.listId === listId)[0];
  660.  
  661. rows.push([
  662. escape(title ? title.toString(): ""),
  663. escape(author ? author.toString(): ""),
  664. escape(link ? link.toString(): ""),
  665. escape(cover ? cover.toString(): ""),
  666. escape(timestamp ? timestamp.toString(): ""),
  667. escape(note.text ? note.text.toString() : ""),
  668. escape(finalRating ? finalRating.toString(): ""),
  669. escape(tags ? tags.join(',').toString(): ""),
  670. escape(index ? index.toString(): ""),
  671. escape(listId ? listId.toString(): ""),
  672. escape(targetList.title ? targetList.title.text.toString() : ""),
  673. escape(item.id ? item.id.toString() : ""),
  674. ])
  675. })
  676. })
  677.  
  678. const latestBackupTime = localStorage.getItem('backupTimeCustom');
  679. const fileSafeBackupTime = latestBackupTime.replace(/[^a-z0-9]/gi, '_').toLowerCase();
  680.  
  681. // CSV handling
  682. let csvString = rows.map(e => e.join(",")).join("\n")
  683. let universalBOM = "\uFEFF";
  684. let csvContent = 'data:text/csv; charset=utf-8,' + encodeURIComponent(universalBOM+csvString);
  685.  
  686. let downloadLinkCsv = document.createElement("a");
  687. downloadLinkCsv.href = csvContent;
  688. downloadLinkCsv.download = `list-backup-user${getUserId()}-${fileSafeBackupTime}.csv`;
  689. downloadLinkCsv.classList.add('c-btn');
  690. downloadLinkCsv.classList.add('download-link');
  691. downloadLinkCsv.id = 'downloadCsv';
  692. downloadLinkCsv.text = 'Download Custom List CSV';
  693.  
  694. $('.info').find("h1").after(downloadLinkCsv);
  695.  
  696. // JSON data handling
  697. let jsonContent = "data:text/json;charset=utf-8," + "\ufeff" + encodeURIComponent(JSON.stringify(allBackup.finalObj));
  698.  
  699. let downloadLinkJson = document.createElement("a");
  700. downloadLinkJson.href = jsonContent;
  701. downloadLinkJson.download = `list-backup-user${getUserId()}-${fileSafeBackupTime}.json`;
  702. downloadLinkJson.classList.add('c-btn');
  703. downloadLinkJson.classList.add('download-link');
  704. downloadLinkJson.id = 'downloadJson';
  705. downloadLinkJson.text = 'Download Custom List JSON';
  706.  
  707. $('.info').find("h1").after(downloadLinkJson);
  708.  
  709. $('.info').find("h1").after(`<span style="display: block; margin-right: 12px; color: #06E8F6; font-size: 16px;">Latest Custom List Backup (${latestBackupTime}): </span>`);
  710.  
  711. $('body').append(`<style>
  712. #downloadJson.c-btn {
  713. top: 70px;
  714. }
  715.  
  716. .download-link {
  717. margin-right: 15px;
  718. margin-top: 12px;
  719. }
  720. </style>`)
  721. }
  722.  
  723. // on sortable list view, when a story card is not visible on the screen, do not load image yet
  724. // load only when the user scrolls onto the said card
  725. function createObserver(targetEl) {
  726. let observer;
  727.  
  728. let options = {
  729. root: null,
  730. rootMargin: "0px",
  731. };
  732.  
  733. // IntersectionObserver is an API that detects elements' visual visibility on screen
  734. // read more about it at https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
  735. observer = new IntersectionObserver((entries, observer) => {
  736. entries.forEach((entry) => {
  737. if (entry.isIntersecting) {
  738. const $el = $(targetEl);
  739. const $wrapper = $el.find('.art-wrapper');
  740. const $img = $wrapper.find('img');
  741. const newSrc = $img.data('src');
  742. $img.attr("src", newSrc);
  743. $wrapper.addClass("img-loaded");
  744. observer.unobserve(targetEl);
  745. }
  746. })
  747. }, options);
  748. observer.observe(targetEl);
  749. }
  750.  
  751. // gets URL parameters in any given link
  752. function getUrlParams() {
  753. const urlSearchParams = new URLSearchParams(window.location.search);
  754. const params = Object.fromEntries(urlSearchParams.entries());
  755.  
  756. return params
  757. }
  758.  
  759. // for fetching existing custom lists' data from local storage
  760. function getCustomLists() {
  761. const listsCreated = decompressString(localStorage.getItem('listsCreated')) || undefined; // FIXME prolly
  762. const listsFollowed = decompressString(localStorage.getItem('listsFollowed')) || undefined; // FIXME prolly
  763.  
  764. const stringified = {
  765. created: listsCreated,
  766. followed: listsFollowed
  767. }
  768.  
  769. const parsed = {
  770. created: listsCreated ? JSON.parse(listsCreated) : undefined,
  771. followed: listsFollowed ? JSON.parse(listsFollowed) : undefined
  772. }
  773.  
  774. return {
  775. stringified,
  776. parsed
  777. };
  778. }
  779.  
  780. // detects if custom list is 'created' or 'followed' + returns index
  781. function getTypeIndexFromCustomList(id) {
  782. if (!id) { return undefined };
  783. const customLists = getCustomLists();
  784.  
  785. const indexCreated = customLists.parsed.created.indexOf(id);
  786.  
  787. if (indexCreated !== -1) {
  788. return {
  789. type: 'x',
  790. index: indexCreated
  791. }
  792. }
  793.  
  794. const indexFollowed = customLists.parsed.followed.indexOf(id);
  795.  
  796. if (indexFollowed !== -1) {
  797. return {
  798. type: 'y',
  799. index: indexFollowed
  800. }
  801. }
  802.  
  803. return {
  804. type: undefined,
  805. index: -1
  806. };
  807. }
  808.  
  809. // gets contents of a list (details and content) given its id
  810. function getListIdContents(id) {
  811. if (!id) {return undefined};
  812.  
  813. const keys = Object.keys(localStorage);
  814.  
  815. const r = new RegExp(`${id}`)
  816. let isContentAvailable = keys.some((key) => {
  817. return key.match(r)
  818. })
  819.  
  820. if (isContentAvailable) {
  821. const check = `${getTypeIndexFromCustomList(id).type}${id}`
  822. const d = `${check}-d`;
  823.  
  824. const mainDetailsKey = keys.filter(key => key === d);
  825. const contentKey = keys.filter(key => key === check);
  826.  
  827. return {
  828. mainDetails: JSON.parse(decompressString(localStorage.getItem(mainDetailsKey))),
  829. content: JSON.parse(decompressString(localStorage.getItem(contentKey))),
  830. }
  831. }
  832.  
  833. return undefined;
  834. }
  835.  
  836. // checks if sortable view caters to a specific custom list
  837. function checkIfCustomSortableMode() {
  838. const urlWithoutParams = getUrlWithoutParams();
  839. if (urlWithoutParams.match(/(manga\/4\/)/gm)) {
  840. const splitUrl = urlWithoutParams.split('/').filter(item => item !== '');
  841.  
  842. let flag = false;
  843.  
  844. if (splitUrl[splitUrl.length - 1] != 4) {
  845. flag = true
  846. }
  847.  
  848. return flag;
  849. } else {
  850. return false
  851. }
  852. }
  853.  
  854. // only start checking for backups/backup-ing when window has finished loading
  855. window.onload = function () {
  856. if (typeof jQuery === "undefined") {
  857. // copied from below, function-ify
  858. // added to catch lists where list is added to custom lists but goes to 404
  859. let typeIndex = getTypeIndexFromCustomList(getListId());
  860.  
  861. let nextTarget = typeIndex.index + 1;
  862.  
  863. const customLists = getCustomLists();
  864. const { parsed } = customLists;
  865. const { created, followed } = parsed;
  866.  
  867. // x = created custom lists
  868. if (typeIndex.type === 'x') {
  869. if (created[nextTarget]) {
  870. let targetUrl = `https://www.mangago.me/home/mangalist/${created[nextTarget]}/?filter=&page=1`;
  871. window.location.replace(targetUrl);
  872. } else {
  873. if (followed[0]) {
  874. let targetUrl = `https://www.mangago.me/home/mangalist/${followed[0]}/?filter=&page=1`;
  875. window.location.replace(targetUrl);
  876. }
  877. }
  878. }
  879.  
  880. // y = followed created lists
  881. if (typeIndex.type === 'y') {
  882. if (followed[nextTarget]) {
  883. let targetUrl = `https://www.mangago.me/home/mangalist/${followed[nextTarget]}/?filter=&page=1`
  884. window.location.replace(targetUrl);
  885. } else {
  886. //finalizing storage keys for ending backup process
  887. localStorage.setItem('backupTimeCustom', new Date().toLocaleString());
  888. localStorage.setItem('backupModeCustom', 'off');
  889. localStorage.removeItem('totalPages');
  890.  
  891. alert('backup done! Redirect to list page after clicking okay');
  892. const userId = localStorage.getItem('backupUser');
  893. localStorage.setItem('generate', 'yes');
  894. window.location.replace(`https://www.mangago.me/home/people/${userId}/list/`)
  895. }
  896. }
  897. } else {
  898. // add custom button styles
  899. $('body').append(`<style>
  900. .c-btn {
  901. display: inline-block;
  902. border: none;
  903. padding: 8px 16px;
  904. text-decoration: none;
  905. background: #0069ed;
  906. color: #ffffff;
  907. font-family: sans-serif;
  908. font-size: 1rem;
  909. cursor: pointer;
  910. text-align: center;
  911. transition: background 250ms ease-in-out,
  912. transform 150ms ease;
  913. -webkit-appearance: none;
  914. -moz-appearance: none;
  915. }
  916.  
  917. .c-btn:hover,
  918. .c-btn:focus {
  919. background: #0053ba;
  920. }
  921.  
  922. .c-btn:focus {
  923. outline: 1px solid #fff;
  924. outline-offset: -4px;
  925. }
  926.  
  927. .c-btn:active {
  928. transform: scale(0.99);
  929. }
  930.  
  931. .c-btn-backup {
  932. padding: 0.5rem 1rem;
  933.  
  934. background-color: green;
  935. }
  936.  
  937. .c-btn-backup:hover {
  938. background-color: #09ab09;
  939. }
  940.  
  941. .c-btn-backup:focus {
  942. background-color: #09ab09;
  943. }
  944.  
  945. .c-btn-reset {
  946. padding: 0.5rem 1rem;
  947.  
  948. background-color: #c74242;
  949. }
  950.  
  951. .c-btn-reset:hover {
  952. background-color: #fb5757;
  953. }
  954.  
  955. .c-btn-reset:focus {
  956. background-color: #fb5757;
  957. }
  958.  
  959. .user-profile h1 {
  960. margin-bottom: 15px;
  961. }
  962.  
  963. .user-profile h1 button {
  964. margin-left: 10px;
  965. }
  966. </style>`)
  967.  
  968. // add check for custom lists
  969. const custom = isForCustomLists() || checkIfCustomSortableMode();
  970.  
  971. const latestBackup = (localStorage.getItem(custom ? 'backupTimeCustom' : 'backupTime'));
  972. // const backupUser = (localStorage.getItem('backupUser'));
  973.  
  974. if (latestBackup && !isForCustomLists() && !checkIfCustomSortableMode()) {
  975. generateDownloadLinksToBackup();
  976.  
  977. appendSortableList();
  978. }
  979.  
  980. // detects if user is on a custom list page and checks if there is current available data for sort view
  981. if (
  982. isForCustomLists()
  983. && getListIdContents(getListId())
  984. && localStorage.getItem('backupModeCustom') === 'off'
  985. && localStorage.getItem('backupUser')
  986. ) {
  987. $('.w-title').find('h1').append(`<button id="customListSortable" class="c-btn" style="margin-left: 12px;">Sortable List</button>`)
  988.  
  989. $('#customListSortable').on('click', () => {
  990. let targetUrl = `https://www.mangago.me/home/people/${localStorage.getItem('backupUser')}/manga/4/${getListId()}/`;
  991. window.location.replace(targetUrl);
  992. })
  993. }
  994.  
  995. // code block responsible for sortable list view
  996. if (window.location.href.match(/(manga\/4\/)/gm)) {
  997. let customListId = undefined;
  998.  
  999. const urlWithoutParams = getUrlWithoutParams();
  1000. const splitUrl = urlWithoutParams.split('/').filter(item => item !== '');
  1001.  
  1002. if (splitUrl[splitUrl.length - 1] !== 4) {
  1003. customListId = splitUrl[splitUrl.length - 1]
  1004. }
  1005.  
  1006. // take into consideration custom mode
  1007. const customListIdContent = checkIfCustomSortableMode() ? getListIdContents(customListId) : undefined;
  1008.  
  1009. const typeIndex = getTypeIndexFromCustomList(customListId);
  1010.  
  1011. const allBackup = customListIdContent ? {
  1012. allData: [{
  1013. arr: customListIdContent.content
  1014. }],
  1015. finalObj: {
  1016. created: typeIndex.type === 'x' ? customListIdContent.content : [],
  1017. followed: typeIndex.type === 'y' ? customListIdContent.content : [],
  1018. mainDetails: [customListIdContent.mainDetails]
  1019. }
  1020. } : getAllBackup();
  1021.  
  1022. if (latestBackup) {
  1023. const allData = allBackup.allData;
  1024.  
  1025. const allTags = []
  1026.  
  1027. allData.forEach(item => {
  1028. item.arr.forEach(subitem => {
  1029. if (subitem.tags) {
  1030. if (subitem.tags.length > 0) {
  1031. subitem.tags.forEach(tag => {
  1032. if (allTags.indexOf(tag) === -1) {
  1033. allTags.push(tag)
  1034. }
  1035. })
  1036. }
  1037. }
  1038. })
  1039. })
  1040.  
  1041. $('body').append(`<div id="floatingNote">
  1042. <div class="note-closer"><span class="emoji emoji274c"></span></div>
  1043. </div>`)
  1044. $('body').append(`<style>
  1045. #floatingNote {
  1046. position: absolute;
  1047. top: 0;
  1048. left: 0;
  1049. width: auto;
  1050. min-width: 250px;
  1051. max-width: 500px;
  1052. height: auto;
  1053. background: #262730;
  1054. }
  1055.  
  1056. #floatingNote.is-active {
  1057. padding: 15px;
  1058. }
  1059.  
  1060. .note-closer .emoji {
  1061. display: none;
  1062. position: absolute;
  1063. top: 0;
  1064. right: 0;
  1065.  
  1066. cursor: pointer;
  1067. }
  1068.  
  1069. #floatingNote.is-active .note-closer .emoji{
  1070. display: block;
  1071. }
  1072.  
  1073. #floatingNote .tag-wrapper {
  1074. margin-top: 12px;
  1075. }
  1076.  
  1077. #floatingNote .tag {
  1078. display: inline-block;
  1079. background-color: #28F;
  1080. border-radius: 2px;
  1081. font-size: 14px;
  1082. color: white;
  1083. padding: 2px;
  1084. margin-right: 2px;
  1085. margin-bottom: 5px;
  1086. }
  1087. </style>`)
  1088.  
  1089. $('.note-closer').on('click', (i, el) => {
  1090. $('#floatingNote').find('.info.summary').remove();
  1091. $('#floatingNote').hide()
  1092. $('#floatingNote').removeClass('is-active')
  1093. })
  1094.  
  1095. // repeats over every single saved story card and creates the HTML for the story card
  1096. const allContentHtml = allData.map(item => {
  1097. return item.arr.map(subitem => {
  1098. const { title, link, cover, timestamp, author, rating, tags, note } = subitem
  1099.  
  1100. let ratingWidth = 0;
  1101.  
  1102. switch (rating) {
  1103. case 1:
  1104. ratingWidth = 11;
  1105. break;
  1106. case 2:
  1107. ratingWidth = 22;
  1108. break;
  1109. case 3:
  1110. ratingWidth = 33;
  1111. break;
  1112. case 4:
  1113. ratingWidth = 44;
  1114. break;
  1115. case 5:
  1116. ratingWidth = 55;
  1117. break;
  1118. default:
  1119. break;
  1120. }
  1121.  
  1122. const pixelPlaceholder = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS7AAAAA1BMVEVHcEyC+tLSAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=`;
  1123.  
  1124. return `<div class="${`element-item-outer ${item.id} ${rating}-star ${
  1125. tags.map(tag => {
  1126. return (cleanAndHyphenateTag(tag)) + " "
  1127. }).join('')
  1128. }`}" data-category="${item.id}">
  1129. ${note.text
  1130. ? `<div class="note-trigger" data-note-html="${
  1131. note.html.replace(/(")/gm, '&quot;')}" data-tags="${tags.join(',').replace(/(")/gm, '&quot;')
  1132. }"></div>`
  1133. : ``
  1134. }
  1135. <div class="element-item-inner">
  1136. <div class="element-item">
  1137. <div class="art-wrapper">
  1138. <a href="${link}">
  1139. <img id="${title.split(/\s/g).join('').replace(/[^a-zA-Z ]/g, '')}" src="${pixelPlaceholder}" data-src="${cover}" alt="${title + ' ' + cover}" />
  1140. </a>
  1141. </div>
  1142. <div class="details">
  1143. <h3 class="title">
  1144. <a href="${link}" title="${title}">
  1145. ${title.substring(0, 40).trim()}${title.length >= 40 ? '...' : ''}
  1146. </a>
  1147. </h3>
  1148. <p class="artist" title="${author}">by ${author.substring(0, 20).trim()}${author.length >= 20 ? '...' : ''}</p>
  1149. <p class="rating" style="display: none;">${rating !== undefined ? rating : -1}</p>
  1150.  
  1151. <div style="display: none;">${note.text}</div>
  1152.  
  1153. ${
  1154. !customListIdContent
  1155. ? rating !== undefined
  1156. ? rating !== -1
  1157. ? `<div class="stars9" id="stars0"><div class="stars9" id="stars5" style="width:${ratingWidth}px;background-position:0 -9px;margin-bottom: 14px;"></div></div>
  1158. <div style="padding: 4px;"></div>`
  1159. : ``
  1160. : ``
  1161. : rating !== undefined
  1162. ? `<div class="non-star-rating">${rating}/10.0</div>`
  1163. : ``
  1164. }
  1165.  
  1166. <div className="tag-wrapper">${
  1167. tags.map(tag => {
  1168. return `<span class="tag">${tag}</span>`
  1169. }).join('')
  1170. }</div>
  1171.  
  1172. <!-- add notes and tags flippable card -->
  1173. <p class="date">${timestamp}</p>
  1174. </div>
  1175. </div>
  1176. </div>
  1177. </div>`
  1178. }).join('')
  1179. }).join('')
  1180.  
  1181.  
  1182. $('body').append(`<style>
  1183. .rightside {
  1184. display: none !important;
  1185. }
  1186.  
  1187. #back_top {
  1188. display: none !important;
  1189. }
  1190.  
  1191. .article {
  1192. width: 100% !important;
  1193. }
  1194.  
  1195. #page.widepage {
  1196. width: calc(100% - 60px);
  1197. }
  1198.  
  1199. .non-star-rating {
  1200. margin-top: -8px;
  1201. color: #FBFA7C;
  1202. margin-bottom: 4px;
  1203. }
  1204. </style>`)
  1205.  
  1206. if (allBackup.finalCount >= idealLimit) {
  1207. $('.article').find('.content').append(`<h1 style="color: #ff7979; margin-bottom: 20px;">Warning: You currently have more than 1000 stories. Sortable view is not optimized for too many stories, hence the degraded performance.</h1>`);
  1208. }
  1209.  
  1210. let mainDetails = undefined;
  1211.  
  1212. if (customListIdContent) {
  1213. mainDetails = customListIdContent.mainDetails;
  1214. }
  1215.  
  1216. // creates the HTML elements needed for the sortable list view filter/sort/search UI
  1217. $('.article').find('.content').append(`
  1218. <div>
  1219. ${
  1220. customListIdContent
  1221. ? `
  1222. <div style="margin-bottom: 20px;">
  1223. <a class="c-btn" href="${`https://www.mangago.me/home/mangalist/${mainDetails.listId}/`}">Return to Current List</a>
  1224. <a class="c-btn" href="${`https://www.mangago.me/home/people/${getUserId()}/list/`}">Return to All Custom Lists</a>
  1225. </div>
  1226.  
  1227. ${mainDetails.title.html}
  1228.  
  1229. <div class="info" style="margin-top: 12px">
  1230. <h2 style="margin-bottom:0">
  1231. <a href="${`https://www.mangago.me/home/people/${mainDetails.curator.id}/home/`}">
  1232. <span style="color: #ececec; font-size: 18px;">curated by</span>
  1233. <span style="text-decoration: underline; color: #06E8F6; font-size: 18px;">${mainDetails.curator.username}</span>
  1234. </a>
  1235. </h2>
  1236. <span>Create: ${mainDetails.created}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Last update: ${mainDetails.updated}</span>
  1237. </div>
  1238. <div style="max-width: 600px; margin-top: 20px;">
  1239. ${mainDetails.description.html}
  1240. </div>
  1241. `
  1242. : ``
  1243. }
  1244. </div>
  1245.  
  1246. <div class="filters">
  1247. <h2>List + Tags</h2>
  1248. <div class="button-group" data-filter-group="default"> <button class="button is-checked" data-filter="">show all</button>
  1249. ${
  1250. !customListIdContent
  1251. ? `<button class="button" data-filter=".wantToRead">Want To Read</button>
  1252. <button class="button" data-filter=".currentlyReading">Currently Reading</button>
  1253. <button class="button" data-filter=".alreadyRead">Already Read</button>`
  1254. : ``
  1255. }
  1256. ${
  1257. allTags.map((tag) => {
  1258. return `<button class="button" data-filter=".${cleanAndHyphenateTag(tag)}">${tag}</button>`
  1259. }).join('')
  1260. }
  1261. </div>
  1262.  
  1263. ${
  1264. !customListIdContent
  1265. ?`<h2>Rating</h2>
  1266. <div class="button-group" data-filter-group="stars"> <button class="button is-checked" data-filter="">any</button>
  1267. <button class="button" data-filter="notUnrated">rated</button>
  1268. <button class="button" data-filter=".5-star">5 &bigstar;</button>
  1269. <button class="button" data-filter=".4-star">4 &bigstar;</button>
  1270. <button class="button" data-filter=".3-star">3 &bigstar;</button>
  1271. <button class="button" data-filter=".2-star">2 &bigstar;</button>
  1272. <button class="button" data-filter=".1-star">1 &bigstar;</button>
  1273. </div>`
  1274. : ``
  1275. }
  1276. </div>
  1277.  
  1278. <h2>Sort</h2>
  1279. <div id="sorts" class="button-group"> <button class="button is-checked" data-sort-by="original-order">original order</button>
  1280. <button class="button" data-sort-by="title" data-sort-direction="asc">
  1281. <span>title</span>
  1282. <span class="chevron bottom"></span>
  1283. </button>
  1284. <button class="button" data-sort-by="date" data-sort-direction="desc">
  1285. <span>date added</span>
  1286. <span class="chevron"></span>
  1287. </button>
  1288. <button class="button" data-sort-by="artist" data-sort-direction="asc">
  1289. <span>author/artist</span>
  1290. <span class="chevron bottom"></span>
  1291. </button>
  1292. <button class="button" data-sort-by="rating" data-sort-direction="desc">
  1293. <span>rating</span>
  1294. <span class="chevron"></span>
  1295. </button>
  1296. </div>
  1297.  
  1298. <h2>Search</h2>
  1299. <p><input type="text" class="quicksearch" placeholder="Search" /></p>
  1300.  
  1301. <p class="filter-count"></p>
  1302.  
  1303. <div class="grid">
  1304. ${allContentHtml}
  1305. </div>
  1306. `)
  1307.  
  1308. $('.note-trigger').each(function( i, el ) {
  1309. var $el = $( el );
  1310.  
  1311. $el.on('click', (e) => {
  1312. const $this = $(this);
  1313.  
  1314. if ($this.is(':visible')) {
  1315. $('#floatingNote').find('.info.summary').remove();
  1316. $('#floatingNote').hide();
  1317. $('#floatingNote').removeClass('is-active');
  1318. }
  1319.  
  1320. const note = $this.data('note-html');
  1321.  
  1322. const tags = $this.data('tags');
  1323. const $floatingNote = $('#floatingNote');
  1324.  
  1325. if ($floatingNote.find('.short-note')[0]) {
  1326. $floatingNote.find('.short-note').remove();
  1327. }
  1328.  
  1329. if ($floatingNote.find('.tag-wrapper')[0]) {
  1330. $floatingNote.find('.tag-wrapper').remove();
  1331. }
  1332. $floatingNote.append(note);
  1333. $floatingNote.append(`<div class="tag-wrapper">
  1334. ${
  1335. tags.length > 0 ? tags.split(',').map(tag => {
  1336. return `<span class="tag">${tag}</span>`
  1337. }).join('') : ''
  1338. }
  1339. </div>`)
  1340. $floatingNote.addClass('is-active');
  1341. $floatingNote.show();
  1342.  
  1343. if (e.pageX > window.innerWidth - 250) { // FIXME: make 250 a variable synced to min-width of floatingNote
  1344. $floatingNote.css('left', e.pageX - 250 + 'px');
  1345. } else {
  1346. $floatingNote.css('left', e.pageX + 'px');
  1347. }
  1348.  
  1349. if (e.clientY > window.innerHeight - $floatingNote.height()) {
  1350. $floatingNote.css('top', e.pageY - $floatingNote.height() + 'px');
  1351. } else {
  1352. $floatingNote.css('top', e.pageY + 'px');
  1353. }
  1354. })
  1355. })
  1356.  
  1357. $('body').append(`<style>
  1358. * { box-sizing: border-box; }
  1359.  
  1360. body {
  1361. font-family: sans-serif;
  1362. }
  1363.  
  1364. /* ---- button ---- */
  1365. .button .chevron {
  1366. border-style: solid;
  1367. border-width: 0.25em 0.25em 0 0;
  1368. content: '';
  1369. display: inline-block;
  1370. height: 0.45em;
  1371. left: 0.15em;
  1372. position: relative;
  1373. top: 0.35em;
  1374. transform: rotate(-45deg);
  1375. vertical-align: top;
  1376. width: 0.45em;
  1377.  
  1378. transition: .2s;
  1379. }
  1380.  
  1381. .button .chevron.bottom {
  1382. transform: rotate(135deg);
  1383. }
  1384.  
  1385. .button {
  1386. display: inline-block;
  1387. padding: 0.5em 1.0em;
  1388. min-height: 40px;
  1389. background: #EEE;
  1390. border: none;
  1391. border-radius: 7px;
  1392. background-image: linear-gradient( to bottom, hsla(0, 0%, 0%, 0), hsla(0, 0%, 0%, 0.2) );
  1393. color: #222;
  1394. font-family: sans-serif;
  1395. font-size: 16px;
  1396. text-shadow: 0 1px white;
  1397. cursor: pointer;
  1398. }
  1399.  
  1400. .button:hover {
  1401. background-color: #8CF;
  1402. text-shadow: 0 1px hsla(0, 0%, 100%, 0.5);
  1403. color: #222;
  1404. }
  1405.  
  1406. .button:active,
  1407. .button.is-checked {
  1408. background-color: #28F;
  1409. }
  1410.  
  1411. .button.is-checked {
  1412. color: white;
  1413. text-shadow: 0 -1px hsla(0, 0%, 0%, 0.8);
  1414. }
  1415.  
  1416. .button:active {
  1417. box-shadow: inset 0 1px 10px hsla(0, 0%, 0%, 0.8);
  1418. }
  1419.  
  1420. /* ---- button-group ---- */
  1421.  
  1422. .button-group {
  1423. margin-bottom: 20px;
  1424. }
  1425.  
  1426. .button-group:after {
  1427. content: '';
  1428. display: block;
  1429. clear: both;
  1430. }
  1431.  
  1432. .button-group .button {
  1433. float: left;
  1434. border-radius: 0;
  1435. margin-left: 0;
  1436. margin-right: 1px;
  1437. }
  1438.  
  1439. .button-group .button:first-child { border-radius: 0.5em 0 0 0.5em; }
  1440. .button-group .button:last-child { border-radius: 0 0.5em 0.5em 0; }
  1441.  
  1442. .content h2 {
  1443. margin-bottom: 10px;
  1444. }
  1445.  
  1446. .quicksearch {
  1447. padding: 5px;
  1448. margin-bottom: 25px;
  1449. font-size: 16px;
  1450. width: 300px;
  1451. height: 40px;
  1452. }
  1453.  
  1454. /* ---- isotope ---- */
  1455.  
  1456. .grid {
  1457. border: 1px solid #333;
  1458. }
  1459.  
  1460. /* clear fix */
  1461. .grid:after {
  1462. content: '';
  1463. display: block;
  1464. clear: both;
  1465. }
  1466.  
  1467. /* ---- .element-item ---- */
  1468.  
  1469. .element-item-outer {
  1470. position: relative;
  1471. width: 250px;
  1472. height: 155px;
  1473.  
  1474. margin: 5px;
  1475.  
  1476. perspective: 1000px;
  1477. }
  1478.  
  1479. .note-trigger {
  1480. position: absolute;
  1481. top: 0;
  1482. right: 0;
  1483. z-index: 2;
  1484. width: 15px;
  1485. height: 15px;
  1486.  
  1487. background-color: #f47dbb;
  1488.  
  1489. cursor: pointer;
  1490. transition: .2s;
  1491. }
  1492.  
  1493. .note-trigger:hover {
  1494. background-color: #ff002f;
  1495. }
  1496.  
  1497. .element-item-inner .is-active {
  1498. transform: rotateY(180deg);
  1499. }
  1500.  
  1501. .element-item-inner {
  1502. position: relative;
  1503. transition: transform 0.5s;
  1504. transform-style: preserve-3d;
  1505. }
  1506.  
  1507. .element-item {
  1508. position: absolute;
  1509. top: 0;
  1510. left: 0;
  1511.  
  1512. -webkit-backface-visibility: hidden; /* Safari */
  1513. backface-visibility: hidden;
  1514. }
  1515.  
  1516. .element-item {
  1517. display: flex;
  1518. width: 250px;
  1519. height: 155px;
  1520. padding: 10px;
  1521. background: #353743;
  1522. color: #262524;
  1523.  
  1524. -webkit-backface-visibility: hidden; /* Safari */
  1525. backface-visibility: hidden;
  1526. }
  1527.  
  1528. .element-item > * {
  1529. margin: 0;
  1530. padding: 0;
  1531. }
  1532.  
  1533. .element-item .title a {
  1534. font-size: 14px;
  1535. color: #06E8F6;
  1536. }
  1537.  
  1538. .element-item .artist {
  1539. margin-bottom: 8px;
  1540. font-size: 12px;
  1541. color: #ddd;
  1542. }
  1543.  
  1544. .element-item img {
  1545. max-width: 90px;
  1546. }
  1547.  
  1548. .element-item .art-wrapper {
  1549. min-width: 90px;
  1550. height: 135px;
  1551. margin-right: 10px;
  1552. overflow: hidden;
  1553. }
  1554.  
  1555. .element-item .details {
  1556. display: flex;
  1557. flex-grow: 1;
  1558. flex-direction: column;
  1559. overflow: hidden;
  1560. }
  1561.  
  1562. .element-item .date {
  1563. font-size: 8px;
  1564. margin-left: auto;
  1565. margin-top: auto;
  1566. color: #b7b7b7;
  1567. }
  1568.  
  1569. .element-item .tag-wrapper {
  1570. display: flex;
  1571. }
  1572.  
  1573. .element-item .tag {
  1574. display: inline-block;
  1575. background-color: #428be7;
  1576. border-radius: 2px;
  1577. font-size: 8px;
  1578. color: white;
  1579. padding: 2px;
  1580. margin-right: 2px;
  1581. margin-bottom: 5px;
  1582. }
  1583.  
  1584. /* .element-item .number {
  1585. position: absolute;
  1586. right: 8px;
  1587. top: 5px;
  1588. } */
  1589. </style>`)
  1590.  
  1591. // Isotope related js
  1592.  
  1593. // store filter for each button group
  1594. var buttonFilters = {};
  1595.  
  1596. // quick search regex
  1597. var qsRegex;
  1598.  
  1599. $('.filters').on( 'click', '.button', function() {
  1600. var $this = $(this);
  1601. // get group key
  1602. var $buttonGroup = $this.parents('.button-group');
  1603. var filterGroup = $buttonGroup.attr('data-filter-group');
  1604. // set filter for group
  1605. buttonFilters[ filterGroup ] = $this.attr('data-filter');
  1606. // Isotope arrange
  1607. $grid.isotope();
  1608. });
  1609.  
  1610. // Initialization of isotope grid. Read more about isotope at: https://isotope.metafizzy.co/
  1611. var $grid = $('.grid').isotope({
  1612. itemSelector: '.element-item-outer',
  1613. layoutMode: 'fitRows',
  1614. getSortData: {
  1615. title: '.title',
  1616. date: '.date',
  1617. artist: '.artist',
  1618. rating: '.rating',
  1619. category: '[data-category]',
  1620. },
  1621. filter: function() {
  1622. var $this = $(this);
  1623. var searchResult = qsRegex ? $this.text().match( qsRegex ) : true;
  1624.  
  1625. var isFilterMatched = true;
  1626.  
  1627. for ( var prop in buttonFilters ) {
  1628. var filter = buttonFilters[ prop ];
  1629. // use function if it matches
  1630. filter = filterFns[ filter ] || filter;
  1631. // test each filter
  1632. if ( filter ) {
  1633. isFilterMatched = isFilterMatched && $(this).is( filter );
  1634. }
  1635. // break if not matched
  1636. if ( !isFilterMatched ) {
  1637. break;
  1638. }
  1639. }
  1640.  
  1641. return searchResult && isFilterMatched;
  1642. }
  1643. });
  1644.  
  1645. var iso = $grid.data('isotope');
  1646. var $filterCount = $('.filter-count');
  1647.  
  1648. function updateFilterCount() {
  1649. $filterCount.text( iso.filteredItems.length + ' items' );
  1650. }
  1651.  
  1652. updateFilterCount();
  1653.  
  1654. $('.element-item-outer').each((i, el) => {
  1655. createObserver(el);
  1656. })
  1657.  
  1658. // filter functions
  1659. var filterFns = {
  1660. notUnrated: function() {
  1661. var number = $(this).find('.rating').text();
  1662. return parseInt(number, 10) !== -1;
  1663. }
  1664. };
  1665.  
  1666. // bind sort button click
  1667. $('#sorts').on( 'click', 'button', function() {
  1668. var $this = $(this);
  1669. var sortByValue = $this.attr('data-sort-by');
  1670. $grid.isotope({ sortBy: sortByValue });
  1671. $grid.isotope();
  1672.  
  1673. updateFilterCount();
  1674. });
  1675.  
  1676. // change is-checked class on buttons
  1677. $('.button-group').each( function( i, buttonGroup ) {
  1678. var $buttonGroup = $( buttonGroup );
  1679. $buttonGroup.on( 'click', 'button', function() {
  1680. $buttonGroup.find('.is-checked').removeClass('is-checked');
  1681. $( this ).addClass('is-checked');
  1682.  
  1683. /* Get the element name to sort */
  1684. var sortValue = $(this).attr('data-sort-by');
  1685.  
  1686. /* Get the sorting direction: asc||desc */
  1687. var direction = $(this).attr('data-sort-direction');
  1688.  
  1689. /* convert it to a boolean */
  1690. var isAscending = (direction == 'asc');
  1691. var newDirection = (isAscending) ? 'desc' : 'asc';
  1692.  
  1693. /* pass it to isotope */
  1694. $grid.isotope({ sortBy: sortValue, sortAscending: isAscending });
  1695.  
  1696. $(this).attr('data-sort-direction', newDirection);
  1697.  
  1698. $(this).find('.chevron').toggleClass('bottom');
  1699. });
  1700.  
  1701. updateFilterCount();
  1702. });
  1703.  
  1704. // use value of search field to filter
  1705. var $quicksearch = $('.quicksearch').keyup( debounce( function() {
  1706. qsRegex = new RegExp( $quicksearch.val(), 'gi' );
  1707. $grid.isotope();
  1708. }, 200 ) );
  1709.  
  1710. // debounce so filtering doesn't happen every millisecond
  1711. function debounce( fn, threshold ) {
  1712. var timeout;
  1713. threshold = threshold || 100;
  1714. return function debounced() {
  1715. clearTimeout( timeout );
  1716. var args = arguments;
  1717. var _this = this;
  1718. function delayed() {
  1719. fn.apply( _this, args );
  1720. }
  1721. timeout = setTimeout( delayed, threshold );
  1722. };
  1723. }
  1724. // end of isotope related js
  1725. }
  1726.  
  1727. } else {
  1728. const userId = getUserId();
  1729. const backupMode = localStorage.getItem(custom ? 'backupModeCustom' : 'backupMode');
  1730.  
  1731. if (userId !== undefined) { // Only do action if user is on an mgg link where userId can be inferred.
  1732. if (backupMode !== 'on') {
  1733. const $userH1 = $('.user-profile').find('h1');
  1734. const btnBackup = `<button id="btnBackup" class="c-btn c-btn-backup">${
  1735. custom ? 'Create New Custom Backup' : 'Create New Backup'
  1736. }</button>`;
  1737. const btnReset = `<button id="btnReset" class="c-btn c-btn-reset">${
  1738. custom ? 'Reset All Custom' : 'Reset All'
  1739. }</button>`;
  1740.  
  1741. if ($userH1[0] !== undefined) {
  1742. $($userH1).append(btnBackup);
  1743. $($userH1).append(btnReset);
  1744. }
  1745.  
  1746. $('#btnBackup').on('click', () => {
  1747. if (custom) {
  1748. localStorage.setItem('backupUser', getUserId());
  1749. }
  1750.  
  1751. const initiateBackup = () => {
  1752. clearListRelatedStorageItems(custom);
  1753.  
  1754. let targetUrl = `https://www.mangago.me/home/people/${userId}/${custom ? 'list/create' : 'manga/1'}/?backup=on`
  1755. window.location.replace(targetUrl);
  1756. }
  1757.  
  1758. if (latestBackup) { // ask for confirmation when a previous successful backup is available
  1759. const proceed = confirm("There is an existing backup. Overwrite?");
  1760.  
  1761. if (proceed) {
  1762. clearLocalStorageItems(custom ? ['backupTimeCustom', 'generate'] : ['backupTime']);
  1763. if (custom) {
  1764. localStorage.removeItem('listsCreated');
  1765. localStorage.removeItem('listsFollowed');
  1766. }
  1767.  
  1768. initiateBackup();
  1769. }
  1770. } else {
  1771. initiateBackup();
  1772. }
  1773. });
  1774.  
  1775. $('#btnReset').on('click', () => {
  1776. if (latestBackup) { // ask for confirmation when a previous successful backup is available
  1777. const proceed = confirm("Confirm reset?");
  1778.  
  1779. if (proceed) {
  1780. clearLocalStorageItems(custom ? ['backupTimeCustom', 'generate'] : ['backupTime']);
  1781. clearListRelatedStorageItems(custom);
  1782.  
  1783. if (custom) {
  1784. localStorage.removeItem('listsCreated');
  1785. localStorage.removeItem('listsFollowed');
  1786. localStorage.removeItem('backupUser');
  1787. }
  1788. window.location.reload();
  1789. }
  1790. } else {
  1791. clearListRelatedStorageItems(custom);
  1792.  
  1793. if (custom) {
  1794. localStorage.removeItem('listsCreated');
  1795. localStorage.removeItem('listsFollowed');
  1796. localStorage.removeItem('backupUser');
  1797. }
  1798. window.location.reload();
  1799. }
  1800. });
  1801. }
  1802.  
  1803. // add custom lists into consideration
  1804. const { backup, page } = getUrlParams();
  1805.  
  1806. const customLists = getCustomLists();
  1807. const listsCreated = customLists.stringified.created
  1808. const listsFollowed = customLists.stringified.followed
  1809.  
  1810. if (backup === 'on') {
  1811. // process for initiating saving content by setting specific storage keys for custom lists
  1812. // get created list ids for backup
  1813. localStorage.setItem(custom ? 'backupModeCustom' : 'backupMode', 'on');
  1814.  
  1815. if (custom) {
  1816. if (getUrlWithoutParams().match(/(create)/gm)) {
  1817. if (!listsCreated) {
  1818. if ($('.left.wrap')[0]) {
  1819. let createdArr = []
  1820.  
  1821. $('.left.wrap').each((i, el) => {
  1822. const $el = $(el);
  1823. let splitUrl = $el.attr('href').split('/');
  1824. splitUrl = splitUrl.filter(item => item !== "");
  1825. const listId = splitUrl[splitUrl.length -1];
  1826. createdArr.push(listId);
  1827. })
  1828.  
  1829. localStorage.setItem('listsCreated', compressString(JSON.stringify(createdArr)));
  1830. window.location.replace(`https://www.mangago.me/home/people/${userId}/list/follow/`);
  1831. } else {
  1832. localStorage.setItem('listsCreated', compressString(JSON.stringify([])));
  1833. window.location.replace(`https://www.mangago.me/home/people/${userId}/list/follow/`);
  1834. }
  1835. }
  1836. }
  1837. } else {
  1838. window.location.replace(`https://www.mangago.me/home/people/${userId}/manga/1/?page=1`);
  1839. }
  1840. }
  1841.  
  1842. // do the same for followed custom lists
  1843. if (backupMode === 'on' && custom && listsCreated && !listsFollowed) {
  1844. if (getUrlWithoutParams().match(/(follow)/gm)) {
  1845. // FIXME: functionify above
  1846. if (!listsFollowed) {
  1847. if ($('.left.wrap')[0]) {
  1848. let followedArr = []
  1849.  
  1850. $('.left.wrap').each((i, el) => {
  1851. const $el = $(el);
  1852. let splitUrl = $el.attr('href').split('/');
  1853. splitUrl = splitUrl.filter(item => item !== "");
  1854. const listId = splitUrl[splitUrl.length -1];
  1855. followedArr.push(listId);
  1856. })
  1857.  
  1858. localStorage.setItem('listsFollowed', compressString(JSON.stringify(followedArr)));
  1859. window.location.reload();
  1860. } else {
  1861. localStorage.setItem('listsFollowed', compressString(JSON.stringify([])));
  1862. window.location.reload();
  1863. }
  1864. }
  1865. }
  1866. }
  1867.  
  1868. // checks where the user should be on flow process for custom lists and redirects accordingly
  1869. if (backupMode === 'on' && listsCreated && listsFollowed) {
  1870. if (JSON.parse(listsCreated).length > 0) {
  1871. window.location.replace(`https://www.mangago.me/home/mangalist/${JSON.parse(listsCreated)[0]}/?filter=&page=1`)
  1872. } else if (JSON.parse(listsFollowed).length > 0) {
  1873. window.location.replace(`https://www.mangago.me/home/mangalist/${JSON.parse(listsFollowed)[0]}/?filter=&page=1`)
  1874. } else {
  1875. alert ('nothing to backup, your created/followed lists are empty');
  1876. }
  1877. }
  1878.  
  1879. // actual start of backup, copied from above, edited for custom lists
  1880. // if (backupMode === 'on') {
  1881. if (backupMode === 'on' && !custom) {
  1882. // gets total pagination numbers
  1883. const totalPagesFromMemory = localStorage.getItem('totalPages');
  1884.  
  1885. // when no total pages are set, get total pages from pagination data first
  1886. if (!totalPagesFromMemory) {
  1887. setTotalPages()
  1888. window.location.reload();
  1889. } else {
  1890. saveList();
  1891.  
  1892. if (page) {
  1893. const currentPage = parseInt(page, 10);
  1894.  
  1895. // will execute saving page by page until it equals the last number on pagination
  1896. if (currentPage < totalPagesFromMemory) {
  1897. const newPage = currentPage + 1;
  1898. const newUrl = getUrlWithoutParams() + `?page=${newPage}`;
  1899. window.location.replace(newUrl);
  1900. } else {
  1901. // when a list type is done, move on to next list
  1902. if (detectList() < 3) {
  1903. localStorage.removeItem('totalPages');
  1904. window.location.replace(`https://www.mangago.me/home/people/${userId}/manga/${detectList() + 1}/?page=1`);
  1905. } else {
  1906. // if last list type is done, set backupTime, and backupUser, generate download links
  1907. localStorage.setItem('backupTime', new Date().toLocaleString());
  1908. localStorage.setItem('backupUser', getUserId());
  1909.  
  1910. generateDownloadLinksToBackup();
  1911.  
  1912. localStorage.setItem('backupMode', 'off');
  1913. localStorage.removeItem('totalPages');
  1914. appendSortableList();
  1915. alert('backup done! page will refresh one more time to reflect download links');
  1916. window.location.reload();
  1917. }
  1918. }
  1919. }
  1920. }
  1921. }
  1922.  
  1923. const generate = localStorage.getItem('generate');
  1924. if (custom && generate === 'yes') {
  1925. generateDownloadLinksToBackupCustom();
  1926. }
  1927. } else {
  1928. if (backupMode === 'on' && getUrlWithoutParams().match(/(mangalist)/gm)) {
  1929. // gets total pagination numbers
  1930. const totalPagesFromMemory = localStorage.getItem('totalPages');
  1931.  
  1932. // when no total pages are set, get total pages from pagination data first
  1933. if (!totalPagesFromMemory) {
  1934. setTotalPages();
  1935. window.location.reload();
  1936. } else {
  1937. let typeIndex = getTypeIndexFromCustomList(getListId());
  1938.  
  1939. if (typeIndex.index !== -1) {
  1940. saveList(custom); //custom type
  1941.  
  1942. const { page } = getUrlParams()
  1943.  
  1944. if (page) {
  1945. const currentPage = parseInt(page, 10);
  1946.  
  1947. if (currentPage < totalPagesFromMemory) {
  1948. // TODO: check if not done
  1949. const newPage = currentPage + 1;
  1950. const newUrl = getUrlWithoutParams() + `?filter=&page=${newPage}`;
  1951. window.location.replace(newUrl);
  1952. } else {
  1953. localStorage.removeItem('totalPages');
  1954.  
  1955. let nextTarget = typeIndex.index + 1;
  1956.  
  1957. const customLists = getCustomLists();
  1958. const { parsed } = customLists;
  1959. const { created, followed } = parsed;
  1960.  
  1961. // x = created custom lists
  1962. if (typeIndex.type === 'x') {
  1963. if (created[nextTarget]) {
  1964. let targetUrl = `https://www.mangago.me/home/mangalist/${created[nextTarget]}/?filter=&page=1`;
  1965. window.location.replace(targetUrl);
  1966. } else {
  1967. if (followed[0]) {
  1968. let targetUrl = `https://www.mangago.me/home/mangalist/${followed[0]}/?filter=&page=1`;
  1969. window.location.replace(targetUrl);
  1970. }
  1971. }
  1972. }
  1973.  
  1974. // y = followed created lists
  1975. if (typeIndex.type === 'y') {
  1976. if (followed[nextTarget]) {
  1977. let targetUrl = `https://www.mangago.me/home/mangalist/${followed[nextTarget]}/?filter=&page=1`
  1978. window.location.replace(targetUrl);
  1979. } else {
  1980. //finalizing storage keys for ending backup process
  1981. localStorage.setItem('backupTimeCustom', new Date().toLocaleString());
  1982. localStorage.setItem('backupModeCustom', 'off');
  1983. localStorage.removeItem('totalPages');
  1984.  
  1985. alert('backup done! Redirect to list page after clicking okay');
  1986. const userId = localStorage.getItem('backupUser');
  1987. localStorage.setItem('generate', 'yes');
  1988. window.location.replace(`https://www.mangago.me/home/people/${userId}/list/`)
  1989. }
  1990. }
  1991. }
  1992. }
  1993. }
  1994. }
  1995. }
  1996. }
  1997. }
  1998. }
  1999. }
  2000. })();