// ==UserScript==
// @name Mangago Backup Lists
// @namespace http://tampermonkey.net/
// @version 1.2.4
// @description Backup your reading lists
// @author You
// @match https://www.mangago.me/*
// @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery.isotope/3.0.6/isotope.pkgd.min.js
// @run-at document-start
// @grant none
// ==/UserScript==
(function() {
'use strict';
// plugins used:
// Isotope - for sortable list view: https://isotope.metafizzy.co/
// lz-string - for string compression: https://pieroxy.net/blog/pages/lz-string/index.html
console.log('mggBackup v.1.2.4');
// Current limit for sortable view. Any more than this results in degraded browser performance.
// Sadly, this is probably the limit of what I can do with this userscript on mgg's site + given its current structure and capability.
// I'm working on getting a separate website up and running so that we can have a more.
// 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 :'<
const idealLimit = 1000
// function that detects how many current pages a specific list has
function setTotalPages() {
const $paginagtion = $('.pagination').first();
let totalPages = undefined
if ($paginagtion[0]) {
totalPages = $paginagtion.attr('total');
} else {
totalPages = 1;
}
localStorage.setItem('totalPages', totalPages);
}
function detectList() {
// detects list by current URL
if (getUrlWithoutParams().match(/(manga\/1\/)/gm)) {
return 1 // manga/1/ = Want To Read
} else if ((getUrlWithoutParams().match(/(manga\/2\/)/gm))) {
return 2 //manga/2/ = Currently Reading
} else if (getUrlWithoutParams().match(/(manga\/3\/)/gm)) {
return 3 //manga/3/ = Already Read
}
return 0
}
// function that gathers details about the stories to save
function saveList(custom = false) {
console.log('saving...');
if (!custom) {
const arr = [];
const $toSave = $('#collection-nav').next().find('.manga');
$toSave.each(function() {
const $this = $(this);
// gets details for each story
const $titleLink = $this.find('.title').find('a');
const title = $titleLink.text();
const author = $this.find('.title').next().find('div').text().split("|")[1].trim();
const link = $titleLink.attr('href');
const cover = $this.find('.cover').find('img').data('src');
let timestamp = '';
let tags = [];
$this.find('.status-rate').find('div').each(function() {
if ($(this).text().match(/(marked)/gm)) {
timestamp = $(this).text().replace(/(marked)/gm, '').trim();
}
if ($(this).text().match(/(Tags)/gm)) {
$(this).find('.tag')?.each(function() {
tags.push($(this).text().trim());
});
}
})
const $note = $this.find('.short-note');
const note = {
text: $note.text().trim(),
html: $note.text().trim() ? $note[0].outerHTML : ``
}
let rating = undefined
const $stars = $this.find('.status-rate').find('.stars9').first().find('.stars9');
let ratingWidth = 0;
if ($stars.css('width')) {
ratingWidth = parseInt($stars.css('width').replace(/(px)/gm, ''));
}
// star ratings are weird because they are not explicitly exposed as 1,2,3,4,5
// so I'm only getting the width of the yellow fill of the stars and assigning each into 1-5 LMAO
switch (ratingWidth) {
case (11):
rating = 1;
break;
case (22):
rating = 2;
break;
case (33):
rating = 3;
break;
case (44):
rating = 4;
break;
case (55):
rating = 5;
break;
default:
rating = undefined;
break;
}
arr.push({
title,
author,
link,
cover,
timestamp,
note,
rating,
tags
});
})
// I turned these into single letter keys for space saving.
const listId = detectList() === 1
? 'w' // want to read
: detectList() === 2
? 'c' // currently reading
: detectList() === 3
? 'd' // done reading
: undefined;
if (listId) {
// this part is the reason why if you check your localStorage on dev tools, there are weird signs
// I am using lz-string to compress strings to be able to save more stories
const compressed = compressString(JSON.stringify(arr))
const { page } = getUrlParams()
localStorage.setItem(`${listId + page}`, compressed);
}
} else {
const params = getUrlParams();
const { page } = params;
const listDetails = {
title: {
text: '',
html: ''
},
listId: '',
curator: {
username: '',
id: '',
},
created: '',
updated: '',
description: {
text: '',
html: ''
},
tags: []
}
if (page) {
if (parseInt(page, 10) === 1) {
const $h1 = $('.w-title').find('h1');
const titleText = $h1.text().trim();
listDetails.title.text = titleText;
listDetails.title.html = titleText ? $h1[0].outerHTML : '';
listDetails.listId = getListId();
const $userProfile = $('.user-profile')
const $info = $userProfile.find('.info');
const curatorLinkParts = $userProfile.find('.pic').find('a').attr('href').split('/').filter(url => url !== '');
const curatorId = curatorLinkParts[curatorLinkParts.length - 1];
listDetails.curator.username = $info.find('h2').text();
listDetails.curator.id = curatorId;
const dates = $info.contents().filter(function(){
return this.nodeType == 3;
})[1].nodeValue.trim().split(': ');
listDetails.updated = dates[2];
listDetails.created = dates[1].split('Last')[0].trim();
const $description = $('.article').find('.description')
const descText = $description.text().trim()
listDetails.description.text = descText;
listDetails.description.html = descText ? $description[0].outerHTML : '';
const tagsArr = []
const $tags = $('.content').find('.tag');
$tags.each((i, el) => {
const $el = $(el);
tagsArr.push($el.text());
})
listDetails.tags = tagsArr;
const listId = getListId();
const type = getTypeIndexFromCustomList(listId).type;
localStorage.setItem(`${type}${listId}-d`, compressString(JSON.stringify(listDetails)));
}
}
const arr = [];
const $toSave = $('.manga.note-and-order');
const $h1 = $('.w-title').find('h1');
const titleText = $h1.text().trim();
const listTitleDetails = {
text: titleText,
html: titleText ? $h1[0].outerHTML : ''
}
$toSave.each(function(i, el) {
const $this = $(el);
// gets details for each story
const $titleLink = $this.find('.title').find('a');
const title = $titleLink.text();
const author = $this.find('.info').filter((i, el) => {
const $el = $(el);
return $el.text().match(/(Author\(s\))/gm);
}).find('span').text();
const link = $titleLink.attr('href');
const cover = $this.find('.cover').find('img').data('src') || $('.album-photos').find('img').first().attr('src') || 'none';
const index = $this.attr('_index');
const tags = $this.find('.info').filter((i, el) => {
const $el = $(el);
return $el.text().match(/(Genre\(s\))/gm);
}).find('span').text().split('/').map(item => item.trim()).filter(item => item !== "");
const $note = $this.find('.info.summary');
const note = {
text: $note.text().trim(),
html: $note.text().trim() ? $note[0].outerHTML : ``
}
const rating = $this.find('.title').next().find('.info').filter((i, el) => {
const $el = $(el);
let returnFlag = false;
if ($el.find('#stars0')[0]) {
returnFlag = true;
}
return returnFlag;
}).find('span').text().trim();
const timestamp = $this.find('.info.summary').next().find('.left').text();
arr.push({
title,
author,
link,
cover,
timestamp,
note,
rating,
tags,
index,
listId: getListId(),
listTitleDetails,
});
});
const listId = getListId();
const type = getTypeIndexFromCustomList(listId).type;
if (listId && type) {
const existing = decompressString(localStorage.getItem(`${type}${listId}`));
if (existing) {
const parsedExisting = JSON.parse(existing);
const newArr = [...parsedExisting, ...arr];
const compressedNewArr = compressString(JSON.stringify(newArr));
localStorage.setItem(`${type}${listId}`, compressedNewArr);
} else {
const compressedNewArr = compressString(JSON.stringify(arr));
localStorage.setItem(`${type}${listId}`, compressedNewArr);
}
}
}
}
// clear localStorage keys where keys begin with w/c/d (for previously saved stories) from probably previous backups
// x is letter for custom lists, y is for followed lists
function clearListRelatedStorageItems (customList = false) {
const keys = Object.keys(localStorage);
let targetArr = [];
if (!customList) {
targetArr = ['w', 'c', 'd']
} else {
targetArr = ['x', 'y']
}
for ( var i = 0, len = localStorage.length; i < len; ++i ) {
const match = targetArr.indexOf(keys[i].charAt(0)) !== -1;
if (match) {
localStorage.removeItem(keys[i]);
}
}
}
// other related items saved
function clearLocalStorageItems (specific = []) {
const items = [
'backupMode', // trigger check for doing page by page backup
'totalPages', // key for total number of pagination per list type
'backupTime', // latest available backup time
'generate', // generate boolean for custom list
]
const finalArrayToTarget = specific.length > 0 ? specific : items
finalArrayToTarget.forEach(item => {
localStorage.removeItem(item)
})
}
// sortable list will be seen as a tab next to [Done Reading] list and can be triggered when number of stories < idealLimit
function appendSortableList() {
const count = getAllBackup().finalCount;
if (!$('#navCustom')[0]) {
$('#collection-nav').append(`
<div id="navCustom" class="nav sub nav-custom">
Sortable List
</div>
`)
$('body').append(`<style>
#collection-nav .nav-custom {
background-color: #0069ed;
color: #ffffff;
cursor: pointer;
transition: .2s;
}
#collection-nav .nav-custom:hover {
background-color: #ffffff;
color: #0069ed;
cursor: pointer;
}
</style>`)
// Clicking the tab redirects to `/manga/4/` an unuse\/4d url, so I just decided to dump backed up stories there
$('#navCustom').on('click', () => {
if (getUserId() !== undefined) {
if (count >= idealLimit) {
const proceed = confirm('Your have more than 1000 stories. The sortable list might be slow, or might not work properly at all. Proceed anyway?')
if (proceed) {
window.location.replace(`https://www.mangago.me/home/people/${getUserId()}/manga/4/`);
}
} else {
window.location.replace(`https://www.mangago.me/home/people/${getUserId()}/manga/4/`);
}
} else {
alert('Error: your userId cannot be obtained. Be sure you are on a url where people/1234567/manga... is visible')
}
})
}
}
// string compression to save space. read more about it over at: https://pieroxy.net/blog/pages/lz-string/index.html
function compressString(string) {
if (LZString) {
return LZString.compressToUTF16(string)
} else {
throw new Error ('lz-string plugin missing')
}
}
function decompressString(string) {
if (LZString) {
return LZString.decompressFromUTF16(string);
} else {
throw new Error ('lz-string plugin missing')
}
}
// Tags are usually filled with emoticons and special characters. cleaning function for tag identifier when filtering sort view
function cleanAndHyphenateTag(tag) {
return tag.replace(/\W/g, '').replace(/ +/g, '-').toLowerCase();
}
// Params are what we see on URLs after the main link. http://sample.com/?paramSample=1
// this can be extracted on the page and this will become a variable named paramSample with a value of 1
function getUrlWithoutParams() {
return window.location.href.split(/[?#]/)[0]
}
// mgg currently has use url formatted like this: https://www.mangago.me/home/people/1234567/
// I'm getting the id part with this function
function getUserId() {
const url = window.location.href.split('/')
const isAturlWithUserId = url.some(urlPart => {
return urlPart === "people"
})
if (isAturlWithUserId) {
const targetIndex = url.indexOf("people") + 1
return url[targetIndex]
} else {
return undefined
}
}
// get Id for custom lists. hereby defining custom lists as any lists that are not want to read/reading/done
function getListId() {
const url = window.location.href.split('/')
const isAturlWithUserId = url.some(urlPart => {
return urlPart === "mangalist"
})
if (isAturlWithUserId) {
const targetIndex = url.indexOf("mangalist") + 1
return url[targetIndex]
} else {
return undefined
}
}
// detect if part of url has pattern pertaining to custom lists
function isForCustomLists() {
const url = window.location.href.split('/');
const isCustom = url.some(urlPart => urlPart === 'list' || urlPart === 'mangalist');
return isCustom;
}
// used when retrieving the stored items into localStorage.
// targets localStorage items that start with w/c/d as set by previous backup
function getListFromStorage(letterId, custom = false, fetchMainDetails = false) {
const keys = Object.keys(localStorage);
let finalKeys = keys.filter((key) => {
if (custom) {
if (fetchMainDetails) {
return key.charAt(0) === letterId && key.match(/(-d)/gm)
} else {
return key.charAt(0) === letterId && !key.match(/(-d)/gm)
}
}
return key.charAt(0) === letterId
})
const finalArr = []
finalKeys.forEach(key => {
finalArr.push(JSON.parse(decompressString(localStorage.getItem(key))))
})
return finalArr.flat()
}
// compiles all lists from storage
function getAllBackup() {
const wantToRead = getListFromStorage('w');
const currentlyReading = getListFromStorage('c');
const alreadyRead = getListFromStorage('d');
const finalObj = {
wantToRead,
currentlyReading,
alreadyRead
}
const allData = [
{
arr: wantToRead,
id: 'wantToRead'
},
{
arr: currentlyReading,
id: 'currentlyReading'
},
{
arr: alreadyRead,
id: 'alreadyRead'
},
]
return {
allData,
finalObj,
finalCount: wantToRead.length + currentlyReading.length + alreadyRead.length
}
}
function getAllBackupCustom() {
const created = getListFromStorage('x', true);
const followed = getListFromStorage('y', true);
const createdDetails = getListFromStorage('x', true, true);
const followedDetails = getListFromStorage('y', true, true);
const finalObj = {
created,
followed,
mainDetails: [...createdDetails, ...followedDetails]
}
const allData = [
{
arr: created,
id: 'created'
},
{
arr: followed,
id: 'followed'
},
]
return {
allData,
finalObj,
}
}
// function for escaping double quotes (") and commas (,) on tricky CSV formats
// see rules over at https://en.wikipedia.org/wiki/Comma-separated_values#Basic_rules
function escape(value) {
if(!['"','\r','\n',','].some(e => value.indexOf(e) !== -1)) {
return value;
}
return '"' + value.replace(/"/g, '""') + '"';
}
// attaches all the fetched data into a clickable link that is shaped like a button
// attached near the header on the user profile page when a previous successful backup is available
function generateDownloadLinksToBackup() {
const rows = []
const allBackup = getAllBackup()
allBackup.allData.forEach(item => {
item.arr.forEach(subitem => {
const { title, author, link, cover, timestamp, note, rating, tags } = subitem
let finalRating = -1
if (rating) {
finalRating = rating
}
rows.push([
escape(title ? title.toString(): ""),
escape(author ? author.toString(): ""),
escape(link ? link.toString(): ""),
escape(cover ? cover.toString(): ""),
escape(timestamp ? timestamp.toString(): ""),
escape(note.text ? note.text.toString() : ""),
escape(finalRating ? finalRating.toString(): ""),
escape(tags ? tags.join(',').toString(): ""),
escape(item.id ? item.id.toString() : ""),
])
})
})
const latestBackupTime = localStorage.getItem('backupTime');
const fileSafeBackupTime = latestBackupTime.replace(/[^a-z0-9]/gi, '_').toLowerCase();
// CSV handling
let csvString = rows.map(e => e.join(",")).join("\n")
let universalBOM = "\uFEFF";
let csvContent = 'data:text/csv; charset=utf-8,' + encodeURIComponent(universalBOM+csvString);
let downloadLinkCsv = document.createElement("a");
downloadLinkCsv.href = csvContent;
downloadLinkCsv.download = `list-backup-user${getUserId()}-${fileSafeBackupTime}.csv`;
downloadLinkCsv.classList.add('c-btn');
downloadLinkCsv.classList.add('download-link');
downloadLinkCsv.id = 'downloadCsv';
downloadLinkCsv.text = 'Download CSV';
$('.info').find("h1").after(downloadLinkCsv);
// JSON data handling
let jsonContent = "data:text/json;charset=utf-8," + "\ufeff" + encodeURIComponent(JSON.stringify(allBackup.finalObj));
let downloadLinkJson = document.createElement("a");
downloadLinkJson.href = jsonContent;
downloadLinkJson.download = `list-backup-user${getUserId()}-${fileSafeBackupTime}.json`;
downloadLinkJson.classList.add('c-btn');
downloadLinkJson.classList.add('download-link');
downloadLinkJson.id = 'downloadJson';
downloadLinkJson.text = 'Download JSON';
$('.info').find("h1").after(downloadLinkJson);
$('.info').find("h1").after(`<span style="display: block; margin-right: 12px; color: #06E8F6; font-size: 16px;">Latest Backup (${latestBackupTime}): </span>`);
$('body').append(`<style>
#downloadJson.c-btn {
top: 70px;
}
.download-link {
margin-right: 15px;
margin-top: 12px;
}
</style>`)
}
// same as above but for custom lists
function generateDownloadLinksToBackupCustom() {
const rows = []
const allBackup = getAllBackupCustom()
const finalObj = allBackup.finalObj
allBackup.allData.forEach(item => {
item.arr.forEach(subitem => {
const { title, author, link, cover, timestamp, note, rating, tags, index, listId } = subitem;
let finalRating = -1;
if (rating) {
finalRating = rating;
}
let targetList = finalObj.mainDetails.filter(item => item.listId === listId)[0];
rows.push([
escape(title ? title.toString(): ""),
escape(author ? author.toString(): ""),
escape(link ? link.toString(): ""),
escape(cover ? cover.toString(): ""),
escape(timestamp ? timestamp.toString(): ""),
escape(note.text ? note.text.toString() : ""),
escape(finalRating ? finalRating.toString(): ""),
escape(tags ? tags.join(',').toString(): ""),
escape(index ? index.toString(): ""),
escape(listId ? listId.toString(): ""),
escape(targetList.title ? targetList.title.text.toString() : ""),
escape(item.id ? item.id.toString() : ""),
])
})
})
const latestBackupTime = localStorage.getItem('backupTimeCustom');
const fileSafeBackupTime = latestBackupTime.replace(/[^a-z0-9]/gi, '_').toLowerCase();
// CSV handling
let csvString = rows.map(e => e.join(",")).join("\n")
let universalBOM = "\uFEFF";
let csvContent = 'data:text/csv; charset=utf-8,' + encodeURIComponent(universalBOM+csvString);
let downloadLinkCsv = document.createElement("a");
downloadLinkCsv.href = csvContent;
downloadLinkCsv.download = `list-backup-user${getUserId()}-${fileSafeBackupTime}.csv`;
downloadLinkCsv.classList.add('c-btn');
downloadLinkCsv.classList.add('download-link');
downloadLinkCsv.id = 'downloadCsv';
downloadLinkCsv.text = 'Download Custom List CSV';
$('.info').find("h1").after(downloadLinkCsv);
// JSON data handling
let jsonContent = "data:text/json;charset=utf-8," + "\ufeff" + encodeURIComponent(JSON.stringify(allBackup.finalObj));
let downloadLinkJson = document.createElement("a");
downloadLinkJson.href = jsonContent;
downloadLinkJson.download = `list-backup-user${getUserId()}-${fileSafeBackupTime}.json`;
downloadLinkJson.classList.add('c-btn');
downloadLinkJson.classList.add('download-link');
downloadLinkJson.id = 'downloadJson';
downloadLinkJson.text = 'Download Custom List JSON';
$('.info').find("h1").after(downloadLinkJson);
$('.info').find("h1").after(`<span style="display: block; margin-right: 12px; color: #06E8F6; font-size: 16px;">Latest Custom List Backup (${latestBackupTime}): </span>`);
$('body').append(`<style>
#downloadJson.c-btn {
top: 70px;
}
.download-link {
margin-right: 15px;
margin-top: 12px;
}
</style>`)
}
// on sortable list view, when a story card is not visible on the screen, do not load image yet
// load only when the user scrolls onto the said card
function createObserver(targetEl) {
let observer;
let options = {
root: null,
rootMargin: "0px",
};
// IntersectionObserver is an API that detects elements' visual visibility on screen
// read more about it at https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const $el = $(targetEl);
const $wrapper = $el.find('.art-wrapper');
const $img = $wrapper.find('img');
const newSrc = $img.data('src');
$img.attr("src", newSrc);
$wrapper.addClass("img-loaded");
observer.unobserve(targetEl);
}
})
}, options);
observer.observe(targetEl);
}
// gets URL parameters in any given link
function getUrlParams() {
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
return params
}
// for fetching existing custom lists' data from local storage
function getCustomLists() {
const listsCreated = decompressString(localStorage.getItem('listsCreated')) || undefined; // FIXME prolly
const listsFollowed = decompressString(localStorage.getItem('listsFollowed')) || undefined; // FIXME prolly
const stringified = {
created: listsCreated,
followed: listsFollowed
}
const parsed = {
created: listsCreated ? JSON.parse(listsCreated) : undefined,
followed: listsFollowed ? JSON.parse(listsFollowed) : undefined
}
return {
stringified,
parsed
};
}
// detects if custom list is 'created' or 'followed' + returns index
function getTypeIndexFromCustomList(id) {
if (!id) { return undefined };
const customLists = getCustomLists();
const indexCreated = customLists.parsed.created.indexOf(id);
if (indexCreated !== -1) {
return {
type: 'x',
index: indexCreated
}
}
const indexFollowed = customLists.parsed.followed.indexOf(id);
if (indexFollowed !== -1) {
return {
type: 'y',
index: indexFollowed
}
}
return {
type: undefined,
index: -1
};
}
// gets contents of a list (details and content) given its id
function getListIdContents(id) {
if (!id) {return undefined};
const keys = Object.keys(localStorage);
const r = new RegExp(`${id}`)
let isContentAvailable = keys.some((key) => {
return key.match(r)
})
if (isContentAvailable) {
const check = `${getTypeIndexFromCustomList(id).type}${id}`
const d = `${check}-d`;
const mainDetailsKey = keys.filter(key => key === d);
const contentKey = keys.filter(key => key === check);
return {
mainDetails: JSON.parse(decompressString(localStorage.getItem(mainDetailsKey))),
content: JSON.parse(decompressString(localStorage.getItem(contentKey))),
}
}
return undefined;
}
// checks if sortable view caters to a specific custom list
function checkIfCustomSortableMode() {
const urlWithoutParams = getUrlWithoutParams();
if (urlWithoutParams.match(/(manga\/4\/)/gm)) {
const splitUrl = urlWithoutParams.split('/').filter(item => item !== '');
let flag = false;
if (splitUrl[splitUrl.length - 1] != 4) {
flag = true
}
return flag;
} else {
return false
}
}
// only start checking for backups/backup-ing when window has finished loading
window.onload = function () {
if (typeof jQuery === "undefined") {
// copied from below, function-ify
// added to catch lists where list is added to custom lists but goes to 404
let typeIndex = getTypeIndexFromCustomList(getListId());
let nextTarget = typeIndex.index + 1;
const customLists = getCustomLists();
const { parsed } = customLists;
const { created, followed } = parsed;
// x = created custom lists
if (typeIndex.type === 'x') {
if (created[nextTarget]) {
let targetUrl = `https://www.mangago.me/home/mangalist/${created[nextTarget]}/?filter=&page=1`;
window.location.replace(targetUrl);
} else {
if (followed[0]) {
let targetUrl = `https://www.mangago.me/home/mangalist/${followed[0]}/?filter=&page=1`;
window.location.replace(targetUrl);
}
}
}
// y = followed created lists
if (typeIndex.type === 'y') {
if (followed[nextTarget]) {
let targetUrl = `https://www.mangago.me/home/mangalist/${followed[nextTarget]}/?filter=&page=1`
window.location.replace(targetUrl);
} else {
//finalizing storage keys for ending backup process
localStorage.setItem('backupTimeCustom', new Date().toLocaleString());
localStorage.setItem('backupModeCustom', 'off');
localStorage.removeItem('totalPages');
alert('backup done! Redirect to list page after clicking okay');
const userId = localStorage.getItem('backupUser');
localStorage.setItem('generate', 'yes');
window.location.replace(`https://www.mangago.me/home/people/${userId}/list/`)
}
}
} else {
// add custom button styles
$('body').append(`<style>
.c-btn {
display: inline-block;
border: none;
padding: 8px 16px;
text-decoration: none;
background: #0069ed;
color: #ffffff;
font-family: sans-serif;
font-size: 1rem;
cursor: pointer;
text-align: center;
transition: background 250ms ease-in-out,
transform 150ms ease;
-webkit-appearance: none;
-moz-appearance: none;
}
.c-btn:hover,
.c-btn:focus {
background: #0053ba;
}
.c-btn:focus {
outline: 1px solid #fff;
outline-offset: -4px;
}
.c-btn:active {
transform: scale(0.99);
}
.c-btn-backup {
padding: 0.5rem 1rem;
background-color: green;
}
.c-btn-backup:hover {
background-color: #09ab09;
}
.c-btn-backup:focus {
background-color: #09ab09;
}
.c-btn-reset {
padding: 0.5rem 1rem;
background-color: #c74242;
}
.c-btn-reset:hover {
background-color: #fb5757;
}
.c-btn-reset:focus {
background-color: #fb5757;
}
.user-profile h1 {
margin-bottom: 15px;
}
.user-profile h1 button {
margin-left: 10px;
}
</style>`)
// add check for custom lists
const custom = isForCustomLists() || checkIfCustomSortableMode();
const latestBackup = (localStorage.getItem(custom ? 'backupTimeCustom' : 'backupTime'));
// const backupUser = (localStorage.getItem('backupUser'));
if (latestBackup && !isForCustomLists() && !checkIfCustomSortableMode()) {
generateDownloadLinksToBackup();
appendSortableList();
}
// detects if user is on a custom list page and checks if there is current available data for sort view
if (
isForCustomLists()
&& getListIdContents(getListId())
&& localStorage.getItem('backupModeCustom') === 'off'
&& localStorage.getItem('backupUser')
) {
$('.w-title').find('h1').append(`<button id="customListSortable" class="c-btn" style="margin-left: 12px;">Sortable List</button>`)
$('#customListSortable').on('click', () => {
let targetUrl = `https://www.mangago.me/home/people/${localStorage.getItem('backupUser')}/manga/4/${getListId()}/`;
window.location.replace(targetUrl);
})
}
// code block responsible for sortable list view
if (window.location.href.match(/(manga\/4\/)/gm)) {
let customListId = undefined;
const urlWithoutParams = getUrlWithoutParams();
const splitUrl = urlWithoutParams.split('/').filter(item => item !== '');
if (splitUrl[splitUrl.length - 1] !== 4) {
customListId = splitUrl[splitUrl.length - 1]
}
// take into consideration custom mode
const customListIdContent = checkIfCustomSortableMode() ? getListIdContents(customListId) : undefined;
const typeIndex = getTypeIndexFromCustomList(customListId);
const allBackup = customListIdContent ? {
allData: [{
arr: customListIdContent.content
}],
finalObj: {
created: typeIndex.type === 'x' ? customListIdContent.content : [],
followed: typeIndex.type === 'y' ? customListIdContent.content : [],
mainDetails: [customListIdContent.mainDetails]
}
} : getAllBackup();
if (latestBackup) {
const allData = allBackup.allData;
const allTags = []
allData.forEach(item => {
item.arr.forEach(subitem => {
if (subitem.tags) {
if (subitem.tags.length > 0) {
subitem.tags.forEach(tag => {
if (allTags.indexOf(tag) === -1) {
allTags.push(tag)
}
})
}
}
})
})
$('body').append(`<div id="floatingNote">
<div class="note-closer"><span class="emoji emoji274c"></span></div>
</div>`)
$('body').append(`<style>
#floatingNote {
position: absolute;
top: 0;
left: 0;
width: auto;
min-width: 250px;
max-width: 500px;
height: auto;
background: #262730;
}
#floatingNote.is-active {
padding: 15px;
}
.note-closer .emoji {
display: none;
position: absolute;
top: 0;
right: 0;
cursor: pointer;
}
#floatingNote.is-active .note-closer .emoji{
display: block;
}
#floatingNote .tag-wrapper {
margin-top: 12px;
}
#floatingNote .tag {
display: inline-block;
background-color: #28F;
border-radius: 2px;
font-size: 14px;
color: white;
padding: 2px;
margin-right: 2px;
margin-bottom: 5px;
}
</style>`)
$('.note-closer').on('click', (i, el) => {
$('#floatingNote').find('.info.summary').remove();
$('#floatingNote').hide()
$('#floatingNote').removeClass('is-active')
})
// repeats over every single saved story card and creates the HTML for the story card
const allContentHtml = allData.map(item => {
return item.arr.map(subitem => {
const { title, link, cover, timestamp, author, rating, tags, note } = subitem
let ratingWidth = 0;
switch (rating) {
case 1:
ratingWidth = 11;
break;
case 2:
ratingWidth = 22;
break;
case 3:
ratingWidth = 33;
break;
case 4:
ratingWidth = 44;
break;
case 5:
ratingWidth = 55;
break;
default:
break;
}
const pixelPlaceholder = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS7AAAAA1BMVEVHcEyC+tLSAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=`;
return `<div class="${`element-item-outer ${item.id} ${rating}-star ${
tags.map(tag => {
return (cleanAndHyphenateTag(tag)) + " "
}).join('')
}`}" data-category="${item.id}">
${note.text
? `<div class="note-trigger" data-note-html="${
note.html.replace(/(")/gm, '"')}" data-tags="${tags.join(',').replace(/(")/gm, '"')
}"></div>`
: ``
}
<div class="element-item-inner">
<div class="element-item">
<div class="art-wrapper">
<a href="${link}">
<img id="${title.split(/\s/g).join('').replace(/[^a-zA-Z ]/g, '')}" src="${pixelPlaceholder}" data-src="${cover}" alt="${title + ' ' + cover}" />
</a>
</div>
<div class="details">
<h3 class="title">
<a href="${link}" title="${title}">
${title.substring(0, 40).trim()}${title.length >= 40 ? '...' : ''}
</a>
</h3>
<p class="artist" title="${author}">by ${author.substring(0, 20).trim()}${author.length >= 20 ? '...' : ''}</p>
<p class="rating" style="display: none;">${rating !== undefined ? rating : -1}</p>
<div style="display: none;">${note.text}</div>
${
!customListIdContent
? rating !== undefined
? rating !== -1
? `<div class="stars9" id="stars0"><div class="stars9" id="stars5" style="width:${ratingWidth}px;background-position:0 -9px;margin-bottom: 14px;"></div></div>
<div style="padding: 4px;"></div>`
: ``
: ``
: rating !== undefined
? `<div class="non-star-rating">${rating}/10.0</div>`
: ``
}
<div className="tag-wrapper">${
tags.map(tag => {
return `<span class="tag">${tag}</span>`
}).join('')
}</div>
<!-- add notes and tags flippable card -->
<p class="date">${timestamp}</p>
</div>
</div>
</div>
</div>`
}).join('')
}).join('')
$('body').append(`<style>
.rightside {
display: none !important;
}
#back_top {
display: none !important;
}
.article {
width: 100% !important;
}
#page.widepage {
width: calc(100% - 60px);
}
.non-star-rating {
margin-top: -8px;
color: #FBFA7C;
margin-bottom: 4px;
}
</style>`)
if (allBackup.finalCount >= idealLimit) {
$('.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>`);
}
let mainDetails = undefined;
if (customListIdContent) {
mainDetails = customListIdContent.mainDetails;
}
// creates the HTML elements needed for the sortable list view filter/sort/search UI
$('.article').find('.content').append(`
<div>
${
customListIdContent
? `
<div style="margin-bottom: 20px;">
<a class="c-btn" href="${`https://www.mangago.me/home/mangalist/${mainDetails.listId}/`}">Return to Current List</a>
<a class="c-btn" href="${`https://www.mangago.me/home/people/${getUserId()}/list/`}">Return to All Custom Lists</a>
</div>
${mainDetails.title.html}
<div class="info" style="margin-top: 12px">
<h2 style="margin-bottom:0">
<a href="${`https://www.mangago.me/home/people/${mainDetails.curator.id}/home/`}">
<span style="color: #ececec; font-size: 18px;">curated by</span>
<span style="text-decoration: underline; color: #06E8F6; font-size: 18px;">${mainDetails.curator.username}</span>
</a>
</h2>
<span>Create: ${mainDetails.created} Last update: ${mainDetails.updated}</span>
</div>
<div style="max-width: 600px; margin-top: 20px;">
${mainDetails.description.html}
</div>
`
: ``
}
</div>
<div class="filters">
<h2>List + Tags</h2>
<div class="button-group" data-filter-group="default"> <button class="button is-checked" data-filter="">show all</button>
${
!customListIdContent
? `<button class="button" data-filter=".wantToRead">Want To Read</button>
<button class="button" data-filter=".currentlyReading">Currently Reading</button>
<button class="button" data-filter=".alreadyRead">Already Read</button>`
: ``
}
${
allTags.map((tag) => {
return `<button class="button" data-filter=".${cleanAndHyphenateTag(tag)}">${tag}</button>`
}).join('')
}
</div>
${
!customListIdContent
?`<h2>Rating</h2>
<div class="button-group" data-filter-group="stars"> <button class="button is-checked" data-filter="">any</button>
<button class="button" data-filter="notUnrated">rated</button>
<button class="button" data-filter=".5-star">5 ★</button>
<button class="button" data-filter=".4-star">4 ★</button>
<button class="button" data-filter=".3-star">3 ★</button>
<button class="button" data-filter=".2-star">2 ★</button>
<button class="button" data-filter=".1-star">1 ★</button>
</div>`
: ``
}
</div>
<h2>Sort</h2>
<div id="sorts" class="button-group"> <button class="button is-checked" data-sort-by="original-order">original order</button>
<button class="button" data-sort-by="title" data-sort-direction="asc">
<span>title</span>
<span class="chevron bottom"></span>
</button>
<button class="button" data-sort-by="date" data-sort-direction="desc">
<span>date added</span>
<span class="chevron"></span>
</button>
<button class="button" data-sort-by="artist" data-sort-direction="asc">
<span>author/artist</span>
<span class="chevron bottom"></span>
</button>
<button class="button" data-sort-by="rating" data-sort-direction="desc">
<span>rating</span>
<span class="chevron"></span>
</button>
</div>
<h2>Search</h2>
<p><input type="text" class="quicksearch" placeholder="Search" /></p>
<p class="filter-count"></p>
<div class="grid">
${allContentHtml}
</div>
`)
$('.note-trigger').each(function( i, el ) {
var $el = $( el );
$el.on('click', (e) => {
const $this = $(this);
if ($this.is(':visible')) {
$('#floatingNote').find('.info.summary').remove();
$('#floatingNote').hide();
$('#floatingNote').removeClass('is-active');
}
const note = $this.data('note-html');
const tags = $this.data('tags');
const $floatingNote = $('#floatingNote');
if ($floatingNote.find('.short-note')[0]) {
$floatingNote.find('.short-note').remove();
}
if ($floatingNote.find('.tag-wrapper')[0]) {
$floatingNote.find('.tag-wrapper').remove();
}
$floatingNote.append(note);
$floatingNote.append(`<div class="tag-wrapper">
${
tags.length > 0 ? tags.split(',').map(tag => {
return `<span class="tag">${tag}</span>`
}).join('') : ''
}
</div>`)
$floatingNote.addClass('is-active');
$floatingNote.show();
if (e.pageX > window.innerWidth - 250) { // FIXME: make 250 a variable synced to min-width of floatingNote
$floatingNote.css('left', e.pageX - 250 + 'px');
} else {
$floatingNote.css('left', e.pageX + 'px');
}
if (e.clientY > window.innerHeight - $floatingNote.height()) {
$floatingNote.css('top', e.pageY - $floatingNote.height() + 'px');
} else {
$floatingNote.css('top', e.pageY + 'px');
}
})
})
$('body').append(`<style>
* { box-sizing: border-box; }
body {
font-family: sans-serif;
}
/* ---- button ---- */
.button .chevron {
border-style: solid;
border-width: 0.25em 0.25em 0 0;
content: '';
display: inline-block;
height: 0.45em;
left: 0.15em;
position: relative;
top: 0.35em;
transform: rotate(-45deg);
vertical-align: top;
width: 0.45em;
transition: .2s;
}
.button .chevron.bottom {
transform: rotate(135deg);
}
.button {
display: inline-block;
padding: 0.5em 1.0em;
min-height: 40px;
background: #EEE;
border: none;
border-radius: 7px;
background-image: linear-gradient( to bottom, hsla(0, 0%, 0%, 0), hsla(0, 0%, 0%, 0.2) );
color: #222;
font-family: sans-serif;
font-size: 16px;
text-shadow: 0 1px white;
cursor: pointer;
}
.button:hover {
background-color: #8CF;
text-shadow: 0 1px hsla(0, 0%, 100%, 0.5);
color: #222;
}
.button:active,
.button.is-checked {
background-color: #28F;
}
.button.is-checked {
color: white;
text-shadow: 0 -1px hsla(0, 0%, 0%, 0.8);
}
.button:active {
box-shadow: inset 0 1px 10px hsla(0, 0%, 0%, 0.8);
}
/* ---- button-group ---- */
.button-group {
margin-bottom: 20px;
}
.button-group:after {
content: '';
display: block;
clear: both;
}
.button-group .button {
float: left;
border-radius: 0;
margin-left: 0;
margin-right: 1px;
}
.button-group .button:first-child { border-radius: 0.5em 0 0 0.5em; }
.button-group .button:last-child { border-radius: 0 0.5em 0.5em 0; }
.content h2 {
margin-bottom: 10px;
}
.quicksearch {
padding: 5px;
margin-bottom: 25px;
font-size: 16px;
width: 300px;
height: 40px;
}
/* ---- isotope ---- */
.grid {
border: 1px solid #333;
}
/* clear fix */
.grid:after {
content: '';
display: block;
clear: both;
}
/* ---- .element-item ---- */
.element-item-outer {
position: relative;
width: 250px;
height: 155px;
margin: 5px;
perspective: 1000px;
}
.note-trigger {
position: absolute;
top: 0;
right: 0;
z-index: 2;
width: 15px;
height: 15px;
background-color: #f47dbb;
cursor: pointer;
transition: .2s;
}
.note-trigger:hover {
background-color: #ff002f;
}
.element-item-inner .is-active {
transform: rotateY(180deg);
}
.element-item-inner {
position: relative;
transition: transform 0.5s;
transform-style: preserve-3d;
}
.element-item {
position: absolute;
top: 0;
left: 0;
-webkit-backface-visibility: hidden; /* Safari */
backface-visibility: hidden;
}
.element-item {
display: flex;
width: 250px;
height: 155px;
padding: 10px;
background: #353743;
color: #262524;
-webkit-backface-visibility: hidden; /* Safari */
backface-visibility: hidden;
}
.element-item > * {
margin: 0;
padding: 0;
}
.element-item .title a {
font-size: 14px;
color: #06E8F6;
}
.element-item .artist {
margin-bottom: 8px;
font-size: 12px;
color: #ddd;
}
.element-item img {
max-width: 90px;
}
.element-item .art-wrapper {
min-width: 90px;
height: 135px;
margin-right: 10px;
overflow: hidden;
}
.element-item .details {
display: flex;
flex-grow: 1;
flex-direction: column;
overflow: hidden;
}
.element-item .date {
font-size: 8px;
margin-left: auto;
margin-top: auto;
color: #b7b7b7;
}
.element-item .tag-wrapper {
display: flex;
}
.element-item .tag {
display: inline-block;
background-color: #428be7;
border-radius: 2px;
font-size: 8px;
color: white;
padding: 2px;
margin-right: 2px;
margin-bottom: 5px;
}
/* .element-item .number {
position: absolute;
right: 8px;
top: 5px;
} */
</style>`)
// Isotope related js
// store filter for each button group
var buttonFilters = {};
// quick search regex
var qsRegex;
$('.filters').on( 'click', '.button', function() {
var $this = $(this);
// get group key
var $buttonGroup = $this.parents('.button-group');
var filterGroup = $buttonGroup.attr('data-filter-group');
// set filter for group
buttonFilters[ filterGroup ] = $this.attr('data-filter');
// Isotope arrange
$grid.isotope();
});
// Initialization of isotope grid. Read more about isotope at: https://isotope.metafizzy.co/
var $grid = $('.grid').isotope({
itemSelector: '.element-item-outer',
layoutMode: 'fitRows',
getSortData: {
title: '.title',
date: '.date',
artist: '.artist',
rating: '.rating',
category: '[data-category]',
},
filter: function() {
var $this = $(this);
var searchResult = qsRegex ? $this.text().match( qsRegex ) : true;
var isFilterMatched = true;
for ( var prop in buttonFilters ) {
var filter = buttonFilters[ prop ];
// use function if it matches
filter = filterFns[ filter ] || filter;
// test each filter
if ( filter ) {
isFilterMatched = isFilterMatched && $(this).is( filter );
}
// break if not matched
if ( !isFilterMatched ) {
break;
}
}
return searchResult && isFilterMatched;
}
});
var iso = $grid.data('isotope');
var $filterCount = $('.filter-count');
function updateFilterCount() {
$filterCount.text( iso.filteredItems.length + ' items' );
}
updateFilterCount();
$('.element-item-outer').each((i, el) => {
createObserver(el);
})
// filter functions
var filterFns = {
notUnrated: function() {
var number = $(this).find('.rating').text();
return parseInt(number, 10) !== -1;
}
};
// bind sort button click
$('#sorts').on( 'click', 'button', function() {
var $this = $(this);
var sortByValue = $this.attr('data-sort-by');
$grid.isotope({ sortBy: sortByValue });
$grid.isotope();
updateFilterCount();
});
// change is-checked class on buttons
$('.button-group').each( function( i, buttonGroup ) {
var $buttonGroup = $( buttonGroup );
$buttonGroup.on( 'click', 'button', function() {
$buttonGroup.find('.is-checked').removeClass('is-checked');
$( this ).addClass('is-checked');
/* Get the element name to sort */
var sortValue = $(this).attr('data-sort-by');
/* Get the sorting direction: asc||desc */
var direction = $(this).attr('data-sort-direction');
/* convert it to a boolean */
var isAscending = (direction == 'asc');
var newDirection = (isAscending) ? 'desc' : 'asc';
/* pass it to isotope */
$grid.isotope({ sortBy: sortValue, sortAscending: isAscending });
$(this).attr('data-sort-direction', newDirection);
$(this).find('.chevron').toggleClass('bottom');
});
updateFilterCount();
});
// use value of search field to filter
var $quicksearch = $('.quicksearch').keyup( debounce( function() {
qsRegex = new RegExp( $quicksearch.val(), 'gi' );
$grid.isotope();
}, 200 ) );
// debounce so filtering doesn't happen every millisecond
function debounce( fn, threshold ) {
var timeout;
threshold = threshold || 100;
return function debounced() {
clearTimeout( timeout );
var args = arguments;
var _this = this;
function delayed() {
fn.apply( _this, args );
}
timeout = setTimeout( delayed, threshold );
};
}
// end of isotope related js
}
} else {
const userId = getUserId();
const backupMode = localStorage.getItem(custom ? 'backupModeCustom' : 'backupMode');
if (userId !== undefined) { // Only do action if user is on an mgg link where userId can be inferred.
if (backupMode !== 'on') {
const $userH1 = $('.user-profile').find('h1');
const btnBackup = `<button id="btnBackup" class="c-btn c-btn-backup">${
custom ? 'Create New Custom Backup' : 'Create New Backup'
}</button>`;
const btnReset = `<button id="btnReset" class="c-btn c-btn-reset">${
custom ? 'Reset All Custom' : 'Reset All'
}</button>`;
if ($userH1[0] !== undefined) {
$($userH1).append(btnBackup);
$($userH1).append(btnReset);
}
$('#btnBackup').on('click', () => {
if (custom) {
localStorage.setItem('backupUser', getUserId());
}
const initiateBackup = () => {
clearListRelatedStorageItems(custom);
let targetUrl = `https://www.mangago.me/home/people/${userId}/${custom ? 'list/create' : 'manga/1'}/?backup=on`
window.location.replace(targetUrl);
}
if (latestBackup) { // ask for confirmation when a previous successful backup is available
const proceed = confirm("There is an existing backup. Overwrite?");
if (proceed) {
clearLocalStorageItems(custom ? ['backupTimeCustom', 'generate'] : ['backupTime']);
if (custom) {
localStorage.removeItem('listsCreated');
localStorage.removeItem('listsFollowed');
}
initiateBackup();
}
} else {
initiateBackup();
}
});
$('#btnReset').on('click', () => {
if (latestBackup) { // ask for confirmation when a previous successful backup is available
const proceed = confirm("Confirm reset?");
if (proceed) {
clearLocalStorageItems(custom ? ['backupTimeCustom', 'generate'] : ['backupTime']);
clearListRelatedStorageItems(custom);
if (custom) {
localStorage.removeItem('listsCreated');
localStorage.removeItem('listsFollowed');
localStorage.removeItem('backupUser');
}
window.location.reload();
}
} else {
clearListRelatedStorageItems(custom);
if (custom) {
localStorage.removeItem('listsCreated');
localStorage.removeItem('listsFollowed');
localStorage.removeItem('backupUser');
}
window.location.reload();
}
});
}
// add custom lists into consideration
const { backup, page } = getUrlParams();
const customLists = getCustomLists();
const listsCreated = customLists.stringified.created
const listsFollowed = customLists.stringified.followed
if (backup === 'on') {
// process for initiating saving content by setting specific storage keys for custom lists
// get created list ids for backup
localStorage.setItem(custom ? 'backupModeCustom' : 'backupMode', 'on');
if (custom) {
if (getUrlWithoutParams().match(/(create)/gm)) {
if (!listsCreated) {
if ($('.left.wrap')[0]) {
let createdArr = []
$('.left.wrap').each((i, el) => {
const $el = $(el);
let splitUrl = $el.attr('href').split('/');
splitUrl = splitUrl.filter(item => item !== "");
const listId = splitUrl[splitUrl.length -1];
createdArr.push(listId);
})
localStorage.setItem('listsCreated', compressString(JSON.stringify(createdArr)));
window.location.replace(`https://www.mangago.me/home/people/${userId}/list/follow/`);
} else {
localStorage.setItem('listsCreated', compressString(JSON.stringify([])));
window.location.replace(`https://www.mangago.me/home/people/${userId}/list/follow/`);
}
}
}
} else {
window.location.replace(`https://www.mangago.me/home/people/${userId}/manga/1/?page=1`);
}
}
// do the same for followed custom lists
if (backupMode === 'on' && custom && listsCreated && !listsFollowed) {
if (getUrlWithoutParams().match(/(follow)/gm)) {
// FIXME: functionify above
if (!listsFollowed) {
if ($('.left.wrap')[0]) {
let followedArr = []
$('.left.wrap').each((i, el) => {
const $el = $(el);
let splitUrl = $el.attr('href').split('/');
splitUrl = splitUrl.filter(item => item !== "");
const listId = splitUrl[splitUrl.length -1];
followedArr.push(listId);
})
localStorage.setItem('listsFollowed', compressString(JSON.stringify(followedArr)));
window.location.reload();
} else {
localStorage.setItem('listsFollowed', compressString(JSON.stringify([])));
window.location.reload();
}
}
}
}
// checks where the user should be on flow process for custom lists and redirects accordingly
if (backupMode === 'on' && listsCreated && listsFollowed) {
if (JSON.parse(listsCreated).length > 0) {
window.location.replace(`https://www.mangago.me/home/mangalist/${JSON.parse(listsCreated)[0]}/?filter=&page=1`)
} else if (JSON.parse(listsFollowed).length > 0) {
window.location.replace(`https://www.mangago.me/home/mangalist/${JSON.parse(listsFollowed)[0]}/?filter=&page=1`)
} else {
alert ('nothing to backup, your created/followed lists are empty');
}
}
// actual start of backup, copied from above, edited for custom lists
// if (backupMode === 'on') {
if (backupMode === 'on' && !custom) {
// gets total pagination numbers
const totalPagesFromMemory = localStorage.getItem('totalPages');
// when no total pages are set, get total pages from pagination data first
if (!totalPagesFromMemory) {
setTotalPages()
window.location.reload();
} else {
saveList();
if (page) {
const currentPage = parseInt(page, 10);
// will execute saving page by page until it equals the last number on pagination
if (currentPage < totalPagesFromMemory) {
const newPage = currentPage + 1;
const newUrl = getUrlWithoutParams() + `?page=${newPage}`;
window.location.replace(newUrl);
} else {
// when a list type is done, move on to next list
if (detectList() < 3) {
localStorage.removeItem('totalPages');
window.location.replace(`https://www.mangago.me/home/people/${userId}/manga/${detectList() + 1}/?page=1`);
} else {
// if last list type is done, set backupTime, and backupUser, generate download links
localStorage.setItem('backupTime', new Date().toLocaleString());
localStorage.setItem('backupUser', getUserId());
generateDownloadLinksToBackup();
localStorage.setItem('backupMode', 'off');
localStorage.removeItem('totalPages');
appendSortableList();
alert('backup done! page will refresh one more time to reflect download links');
window.location.reload();
}
}
}
}
}
const generate = localStorage.getItem('generate');
if (custom && generate === 'yes') {
generateDownloadLinksToBackupCustom();
}
} else {
if (backupMode === 'on' && getUrlWithoutParams().match(/(mangalist)/gm)) {
// gets total pagination numbers
const totalPagesFromMemory = localStorage.getItem('totalPages');
// when no total pages are set, get total pages from pagination data first
if (!totalPagesFromMemory) {
setTotalPages();
window.location.reload();
} else {
let typeIndex = getTypeIndexFromCustomList(getListId());
if (typeIndex.index !== -1) {
saveList(custom); //custom type
const { page } = getUrlParams()
if (page) {
const currentPage = parseInt(page, 10);
if (currentPage < totalPagesFromMemory) {
// TODO: check if not done
const newPage = currentPage + 1;
const newUrl = getUrlWithoutParams() + `?filter=&page=${newPage}`;
window.location.replace(newUrl);
} else {
localStorage.removeItem('totalPages');
let nextTarget = typeIndex.index + 1;
const customLists = getCustomLists();
const { parsed } = customLists;
const { created, followed } = parsed;
// x = created custom lists
if (typeIndex.type === 'x') {
if (created[nextTarget]) {
let targetUrl = `https://www.mangago.me/home/mangalist/${created[nextTarget]}/?filter=&page=1`;
window.location.replace(targetUrl);
} else {
if (followed[0]) {
let targetUrl = `https://www.mangago.me/home/mangalist/${followed[0]}/?filter=&page=1`;
window.location.replace(targetUrl);
}
}
}
// y = followed created lists
if (typeIndex.type === 'y') {
if (followed[nextTarget]) {
let targetUrl = `https://www.mangago.me/home/mangalist/${followed[nextTarget]}/?filter=&page=1`
window.location.replace(targetUrl);
} else {
//finalizing storage keys for ending backup process
localStorage.setItem('backupTimeCustom', new Date().toLocaleString());
localStorage.setItem('backupModeCustom', 'off');
localStorage.removeItem('totalPages');
alert('backup done! Redirect to list page after clicking okay');
const userId = localStorage.getItem('backupUser');
localStorage.setItem('generate', 'yes');
window.location.replace(`https://www.mangago.me/home/people/${userId}/list/`)
}
}
}
}
}
}
}
}
}
}
}
})();