// ==UserScript==
// @name Mangadex Plus
// @namespace https://greasyfork.org/users/553660
// @icon https://mangadex.org/images/misc/navbar.svg
// @version 1.2.5
// @description Adds extra features to Mangadex. These include: Custom Folders for Manga, Start & Continue Reading Button, Mark all Chapters as read/unread Button, Automatic Chapter Preloading & more to come!
// @author Mr. M
// @match https://mangadex.org/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// @downloadURL https://update.greasyfork.icu/scripts/412392/Mangadex%20Plus.user.js
// @updateURL https://update.greasyfork.icu/scripts/412392/Mangadex%20Plus.meta.js
// ==/UserScript==
(function() {
'use strict';
/*
Global values
*/
const domain = window.location.hostname;
const url = window.location;
const nVer = "1.2";
const current_user = document.getElementsByClassName("navbar-nav")[1].childNodes[3].childNodes[1].getAttribute("href").split("/")[2];
var api_base_url;
try{
if (JSON.parse(GM_getValue("options")).options[0].active_api_url == 0){
api_base_url = "https://mangadex.org/api/v2/";
}
else if (JSON.parse(GM_getValue("options")).options[0].active_api_url == 1){
api_base_url = "https://api.mangadex.org/v2/"
}
else{
api_base_url = "https://api.mangadex.org/v2/";
}
}
catch(e){
console.log(e);
api_base_url = "https://api.mangadex.org/v2/";
}
console.log("Mangadex Plus >> Base API URL: " + api_base_url)
//var api_base_url = "https://mangadex.org/api/v2/";
/*
Debug override
*/
var title_entry_duplication = false; // true = allow dupes
/*
Global functions
*/
// Get the domain from the visiting website
function url_domain(data) {
var a = document.createElement('a');
a.href = data;
return a.hostname;
}
// Add a global CSS style by inputting a String
function addGlobalStyle(css) {
var head, style;
head = document.getElementsByTagName('head')[0];
if (!head) { return; }
style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = css;
head.appendChild(style);
}
// Get site subpage name/directory from provided url
function getSubPage(directoryIndex){
let url = document.location.href;
var segment = url.replace(/^https?:\/\//, '').split('/')[directoryIndex];
return segment;
}
// Get current Url
function currentUrl(){
return document.location.href;
}
/*
Storage defaults
*/
let first_time;
if (GM_getValue("first_time") != "false"){
first_time = false;
GM_setValue("first_time", "false");
//GM_setValue("folders_index", 0);
let folders = {"folders" : []};
GM_setValue("folders", JSON.stringify(folders));
let options = {"options" : [{"preloading" : 0}]};
GM_setValue("options", JSON.stringify(options));
alert("Thank you for installing Mangadex Plus! Please go to Settings -> About to learn more about Mangadex Plus!")
}
// Version check (last version / newest version)
if (GM_getValue("version") != nVer && first_time == undefined){
GM_setValue("version", nVer);
GM_setValue("update_notif", "true")
alert("A feature update is here! MD+ has been updated to " + GM_getValue("version") + "! Check Change Log in MD+ Settings for more info!");
}
var red_badge;
if (GM_getValue("update_notif") == "true"){
GM_setValue("notif_badge", ' ! ')
}
else{
GM_setValue("notif_badge", '')
}
if (JSON.parse(GM_getValue("options")).options[0].active_api_url == undefined){
var temp_options = JSON.parse(GM_getValue("options")).options[0];
temp_options.active_api_url = 1;
temp_options = {"options": [temp_options]};
GM_setValue("options", JSON.stringify(temp_options));
}
else if (JSON.parse(GM_getValue("options")).options[0].active_api_url == 0){
alert("To Mangadex Plus user: \n\nMangadex has decided that the Old API URL will be removed in the coming week (as of 2021-03-09). In order for the script to continue working you must update your Base API " +
"Url in the options of Mangadex Plus. \n\n!!! Please click the \"MD Plus\" button, select Options and switch the \"Base API URL\" option from \"Old\" to \"New\". " +
"Do not forget to press the Save button for the changes to apply. THIS MESSAGE WILL BE PERSISTENT AND REAPPEAR IF YOU DON'T CHANGE THIS OPTION.");
}
/*
Site functions
*/
// Startup functions
function startup(){
if (getSubPage(1) == "title"){
main("title");
}
else if (getSubPage(1) == "chapter"){
main("chapter")
}
else{
main("other");
}
}
/*
HTML Elements
*/
// Color categorizing CSS classes -> "badge" || "text" + "-" + "success"/"danger"/"primary"/nothing
//let friends_icon = '';
//red_badge = ' ! ';
//let green_badge = ' ! ';
//let msg_icon = '';
let container = document.getElementsByClassName("navbar")[0];
let pageMask = '
';
let closeButton = ' To Exit click anywhere or press Esc.';
/*
Functions and features
*/
function main(level){
let menuButton = '
" +
"\"What is Mangadex Plus?\" Mangadex Plus is a Userscript that adds some useful features to Mangadex.
" +
"\"What are these Useful features you speak of?\" These currently include:" +
"
Custom folders
Marking all chapters of a Manga as read/unread
Start reading button
" +
"
Automatic Chapter Preloading
" +
"
For more info on the features head to the Change Log. As of now these are all but more features will be added through time.
" +
"\"I really want a feature to be added to this script! Do you take suggestions?\" I do so Welcomely. Please go to the GreasyFork page of this Userscript and add a suggestion there.
" +
"\"Hey! I found a bug! Where can I report It?\" Please report bugs on the GreasyFork page of this Userscript" +
" and I'll do my best to fix them ASAP.
" +
'
To remove the red badge ( ! ) please reload the page.';
let element = '
" +
"A new option for setting the active API base URL has been added. If you don't understand what an API is you can ignore this message." +
"\nFor those that are still interested here is the rundown why this was added.\n\n" +
"Mangadex has now (actually a little while ago) established a new server that handles just API requests and therefore set a new URL for accessing It. The problem is the new API server has some bugs...\n"+
"The bug that I'm concerned with is just visual and may make you think the script is not working. When you press the button to set all chapters as read you expect all the icons to change to read. " +
"Well, the new API has some \"caching\" issues and doesn't apply the change visually. The chapters will be set as read/unread but the icons won't display that change until you interact with them and refresh." +
"This issue has now been present for quite a while so I decided to wait out the patch. It appears the patch will take some time and I don't know when the old API URL will get shut down so just in case " +
"I added a feature that allows you to change the API's base URL when I can't publish an update.\n\n" +
"By default, the option will be set to the \"Old\" URL, but if you experience issues with the script it may be related to this so in that case, I advise you to change the option to the \"New\" URL." +
"
TL;DR: Mangadex has a new API URL, the Older one is better (less bugs) but may stop working in the future. If something breaks in the script changing the option of the API URL to the " +
"New URL may fix the issue." +
"
To remove the red badge ( ! ) please reload the page.
" +
"Version 1.2 (10-Dec-2020)
" +
"Bug fixes and improvements.
Bug Fix: Folders with many entries now have a scroll wheel. Before the entries would overflow out of the screen
" +
"
Bug Fix/Improvement: Before Mark As Read/Unread would apply only to one page of chapters (The page that is being visited). This wasn't really a bug " +
"as It was an oversight by me while making the script not including this feature.
Feature (per request).
Continue reading: " +
"Start reading button now has a secondary feature (accessed by the new dropdown) that allows the user to continue where he left off. Important to note: " +
"The script determines the chapter qualification based on the read/unread markers seen on the chapter list. Meaning you have to be logged in and follow the manga.
" +
"Regarding the Continue Reading button: In the future, I hope to add various types of chapter qualification determination. (Specifically: The reading progress indicator present on the title's page). " +
"While I have new features in the works I can't always work on them (either busy or lazy) nor can I release them because of their instability. " +
"But smaller feature requests like the one today can be prioritized and posted quicker. So If you also have any feel free to request them!" +
"Version 1.1 (3-Nov-2020)
" +
"Bug fixes and improvements.
The Folders window/box now has the same width as all other windows.
" +
"
Scrolling through the site while any MD+ windows are open is now disabled.
" +
"
Bug fix: Now saving titles to folders only up to the ID of the link making it independent from title changes and immune to entry duplicating.
" +
"
Changed the Userscript's description and added links to the Userscript's GreasyFork page " +
"in the About and Change Log sections.
" +
"I found out that mangadex is soon updating their API to v2 which will hopefully allow me to add some interesting features to the script. " +
"I won't release these features (or even start coding) them for now because they are likely to break/stop working after the API is updated." +
"Mangadex Plus version 1.0 is here! (3-Oct-2020)
" +
"The very first version brings these features to the table:" +
"
Custom folders: You can make folders and add your chosen titles to It.
Mark all chapters of a Manga as read/unread: " +
"Saves you the trouble of having to mark all chapters individually.
Start reading button: Goes to the first chapter of a Manga without having to scroll or switch pages.
" +
"
Automatic Chapter Preloading: Whenever you are in the reader (and are logged into your MD Account) the chapter will automatically start preloading. " +
"You can turn this feature on/off in th Options Menu.
" +
"I am planning on improving the current features and adding new ones. If you have a suggestion/bug report please let me know on the " +
"Userscript's GreasyFork page forum.";
let element = '
' +
'
' +
'
' +
' Change Log
' +
'
' +
'
' +
'
' + text + '
' +
'' +
'
';
closeMenu();
container.insertAdjacentHTML("afterbegin", element);
addGlobalStyle("div.plus-box { position: fixed !important; right: 700px !important; left: 700px !important; top: 100px !important; z-index: 100001;}");
container.insertAdjacentHTML("afterbegin", pageMask);
addGlobalStyle(".page-mask { background: rgba(0, 0, 0, 0.5); position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 100000;}");
container.insertAdjacentHTML("afterbegin", closeButton);
addGlobalStyle(".plus-clsBtn { right: 150px !important; position: fixed !important; z-index: 100000; width: auto; color: white; background-color: #444;}");
document.getElementsByClassName("plus-back-settings")[0].addEventListener("click", function(){closeMenu(); openSettingsMenu();});
document.getElementsByClassName("page-mask")[0].addEventListener("click", closeMenu);
document.getElementsByClassName("plus-clsBtn")[0].addEventListener("click", closeMenu);
document.getElementsByClassName("plus-close")[0].addEventListener("click", closeMenu);
addGlobalStyle("body { overflow: hidden;");
}
function resetMDP(level){
function prompt(level){
let text_rw = "WARNING: By confirming this you will DELETE ALL Your Mangadex Plus saved data! This includes options and folders! \nAre you sure you want to proceed?";
let text_r = "WARNING: By confirming this you will RESET ALL Your Mangadex Plus options to Default values! \nAre you sure you want to proceed?";
let text_w = "WARNING: By confirming this you will WIPE ALL Your Mangadex Plus folders and It's Entries! \nAre you sure you want to proceed?";
if (level == "rw"){
if (confirm(text_rw)) {
resets("rw");
}
}
else if (level == "r"){
if (confirm(text_r)) {
resets("r");
}
}
else if (level == "w"){
if (confirm(text_w)) {
resets("w");
}
}
}
prompt(level);
}
function checkKey(e) {
e = e || window.event;
if (e.keyCode == '27' || e.keyCode == '27') {
closeMenu();
}
}
document.onkeydown = checkKey;
document.getElementsByClassName("plus-settings")[0].addEventListener("click", openSettingsMenu);
}
function folders(){
function getFolders(){
let folders = (JSON.parse(GM_getValue("folders"))).folders;
let backFolder = '
..
'
let createFolder = '
Create New Folder
'
let otherFolders = "";
let edit_btn = '
' +
'
';
let delete_btn = '
' +
'
';
for (let i = 0; i < folders.length; i++){
otherFolders = otherFolders + '
' +
' ' + folders[i].name + '
' + edit_btn + delete_btn + '
';
}
return backFolder + otherFolders + createFolder;
}
function folderItems(index){
let backFolder = '
..
'
let folders = (JSON.parse(GM_getValue("folders"))).folders;
let folder_name = (JSON.parse(GM_getValue("folders"))).folders[index].name;
let delete_btn = '
' +
'
';
let html = "";
for (let i = 0; i < (folders[index].entries).length; i++){
html = html + '
(*) Automatically starts preloading chapter when in the reader. Only works for logged-in users. ' +
'(**) Default is set to Old. If you experience any issues with buttons not working try setting this to New.
';
let options = //'
' +
'
' +
'
' +
'
' +
' Mangadex Plus Options' +
'
' +
'' +
'
' +
'
' +
'' +
'
' +
'' +
'
';
//'
';
function listeners(){
//Listeners for options buttons
for(let i = 0; i < document.getElementsByClassName("plus-preloading-options")[0].getElementsByTagName("a").length; i++){
document.getElementsByClassName("plus-preloading-options")[0].getElementsByTagName("a")[i].addEventListener("click", function(){selectOption("preloading", i)});
}
for(let i = 0; i < document.getElementsByClassName("plus-base_api-options")[0].getElementsByTagName("a").length; i++){
document.getElementsByClassName("plus-base_api-options")[0].getElementsByTagName("a")[i].addEventListener("click", function(){selectOption("active_api_url", i)});
}
document.getElementsByClassName("plus-options-save")[0].addEventListener("click", saveOptions);
}
function openOptionsMenu(){
container.insertAdjacentHTML("afterbegin", options);
addGlobalStyle("div.plus-box { position: fixed !important; right: 700px !important; left: 700px !important; top: 100px !important; z-index: 100001;}");
container.insertAdjacentHTML("afterbegin", pageMask);
addGlobalStyle(".page-mask { background: rgba(0, 0, 0, 0.5); position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 100000;}");
container.insertAdjacentHTML("afterbegin", closeButton);
addGlobalStyle(".plus-clsBtn { right: 150px !important; position: fixed !important; z-index: 100000; width: auto; color: white; background-color: #444;}");
document.getElementsByClassName("page-mask")[0].addEventListener("click", closeMenu);
document.getElementsByClassName("plus-clsBtn")[0].addEventListener("click", closeMenu);
document.getElementsByClassName("plus-close")[0].addEventListener("click", closeMenu);
addGlobalStyle("body { overflow: hidden;");
loadOptions();
listeners();
}
function checkKey(e) {
e = e || window.event;
if (e.keyCode == '27' || e.keyCode == '27') {
closeMenu();
}
}
let sample_options = {
"options" : [
{
"preloading" : 0,
"base_api" : 0
}
]
}
function saveOptions(){
GM_setValue("options", "{\"options\":[" + JSON.stringify(temp_storage) + "]}")
location.reload()
}
function loadOptions(){
var options = JSON.parse(GM_getValue("options"));
let preloading = options.options[0].preloading;
let base_api = options.options[0].active_api_url;
selectOption("preloading", preloading);
selectOption("active_api_url", base_api);
}
function selectOption(option, index){
if(option == "preloading"){
let val;
for(let i = 0; i < document.getElementsByClassName("plus-preloading-options")[0].getElementsByTagName("a").length; i++){
try{
document.getElementsByClassName("plus-preloading-options")[0].getElementsByTagName("a")[i].classList.remove('selected');
document.getElementsByClassName("plus-preloading-options")[0].getElementsByTagName("a")[i].classList.remove('active');
val = document.getElementsByClassName("plus-preloading-options")[0].getElementsByTagName("a")[index].childNodes[1].innerHTML;
}
catch(e){/*Nothing*/}
}
document.getElementsByClassName("plus-preloading-options")[0].getElementsByTagName("a")[index].classList.add("active");
document.getElementsByClassName("plus-preloading-options")[0].getElementsByTagName("a")[index].classList.add("selected");
document.getElementsByClassName("plus-preloading-selected")[0].innerHTML = val;
temp_storage.preloading = index;
}
if(option == "active_api_url"){
let val;
for(let i = 0; i < document.getElementsByClassName("plus-base_api-options")[0].getElementsByTagName("a").length; i++){
try{
document.getElementsByClassName("plus-base_api-options")[0].getElementsByTagName("a")[i].classList.remove('selected');
document.getElementsByClassName("plus-base_api-options")[0].getElementsByTagName("a")[i].classList.remove('active');
val = document.getElementsByClassName("plus-base_api-options")[0].getElementsByTagName("a")[index].childNodes[1].innerHTML;
}
catch(e){/*Nothing*/}
}
document.getElementsByClassName("plus-base_api-options")[0].getElementsByTagName("a")[index].classList.add("active");
document.getElementsByClassName("plus-base_api-options")[0].getElementsByTagName("a")[index].classList.add("selected");
document.getElementsByClassName("plus-base_api-selected")[0].innerHTML = val;
temp_storage.active_api_url = index;
}
}
document.onkeydown = checkKey;
document.getElementsByClassName("plus-options")[0].addEventListener("click", openOptionsMenu);
}
function actionsBar(){
function makeBar(){
let bar = '
' +
'
Mangadex+ Actions:
' +
'
' +
'
' +
'
';
var position = document.getElementsByClassName("col-xl-9 col-lg-8 col-md-7")[0];
position.insertAdjacentHTML("beforeend", bar);
}
function addToFolder(){
function addEntry(index){
let folders = JSON.parse(GM_getValue("folders"));
let link = "https://" + domain + "/title/" + getSubPage(2).replace("#", "");
let pass = true;
for(let i = 0; i < folders.folders[index].entries.length; i++){
try{
if(link == folders.folders[index].entries[i].link){
pass = false;
}
}
catch(e){
alert(3)
if (e == TypeError){
pass = true;
}
}
}
if (pass || title_entry_duplication){
folders.folders[index].entries[folders.folders[index].entries.length] = {"title": "" + document.getElementsByClassName("card-header")[0].childNodes[3].innerHTML, "link" : "" + link};
GM_setValue("folders","" + JSON.stringify(folders));
}
}
function listFolders(){
let folders = JSON.parse(GM_getValue("folders"));
let html = "";
for (let i = 0; i < folders.folders.length; i++){
html = html + '
' + folders.folders[i].name + '
';
}
return html;
}
let bar = '
' +
'' +
'
' +
listFolders() +
'
' +
'
 ';
let position2 = document.getElementsByClassName("plus-actionbar")[0];
position2.insertAdjacentHTML("beforeend", bar);
let folders = JSON.parse(GM_getValue("folders"));
for (let i = 0; i < folders.folders.length; i++){
document.getElementsByClassName("plus-add-folder-" + i)[0].addEventListener("click", function(){addEntry(i)});
}
function reloadFolders(){
let index = document.getElementsByClassName("plus-actions-folders")[0].childNodes.length
for(let i = 0; i < index; i++){
document.getElementsByClassName("plus-add-folder-" + i)[0].remove();
}
let position3 = document.getElementsByClassName("plus-actions-folders")[0];
position3.insertAdjacentHTML("afterbegin", listFolders());
let folders = JSON.parse(GM_getValue("folders"));
for (let i = 0; i < folders.folders.length; i++){
document.getElementsByClassName("plus-add-folder-" + i)[0].addEventListener("click", function(){addEntry(i)});
}
}
document.getElementsByClassName("plus-folder-trigger")[0].addEventListener("click", reloadFolders);
}
function markAs(){
function mark(para){
document.getElementsByClassName("plus-read-pos")[0].classList.remove("fa-eye");
document.getElementsByClassName("plus-read-pos")[0].classList.add("fa-spinner");
document.getElementsByClassName("plus-read-pos")[0].classList.add("fa-pulse");
let api = api_base_url + "user/" + current_user + "/marker"; // API link for posting request to change read status
//let api = api_base_url + "user/me/marker";
let manga = getSubPage(2).replace("#", ""); // Current manga id
let api2 = api_base_url + "manga/" + manga + "/chapters"; // API link for chapter ID's of this manga
async function getChapters(){ // Gets all english chapters
var chapters_ = [];
let response = await fetch(api2);
var data = await response.json();
var length = data.data.chapters.length;
for (let i = 0; i < length; i++){
if (await data.data.chapters[length-i-1].language == "gb"){
await chapters_.push(data.data.chapters[length-i-1].id);
}
}
return chapters_;
}
let param;
getChapters().then(x => {
var chapters_ = x;
async function post(chapters_, check){ // Posting the request to the API (check -> if it's the last request and passes page reload if it is)
// post body data
console.log(para, chapters_)
param = {
read : para,
chapters : chapters_
};
// request options
options = {
method: 'POST',
body: JSON.stringify(param),
headers: {
'Content-Type': 'application/json' //Mangadex requires this
},
credentials: "include"
}
// send POST request
fetch(api, options)
.then(res => res.json())
.then(res => console.log(res))
.then(function(){if(check == true){
document.location.reload() //Reloads page so the changes can be seen
}
}
);
}
async function countHandler(chapters_){ // Mangadex API limits 100 changes per request. This handles making enough requests of which the maximum is 100
let len = (parseInt(chapters_.length/100)+1)
if (chapters_.length > 100){
for(let i = 0; i < len; i++){
if(i == len-1){
await post(chapters_, true)
}
else{
await post(chapters_, false);
chapters_.splice(0, 100);
}
}
}
else{
await post(chapters_, true)
}
}
countHandler(chapters_);
})
}
let mark_button = '
' +
'' +
'
' +
'
' +
' Read
' +
'
' +
' Unread
' +
'
' +
'
 ';
let position2 = document.getElementsByClassName("plus-actionbar")[0];
position2.insertAdjacentHTML("beforeend", mark_button);
document.getElementsByClassName("plus-mark-read")[0].addEventListener("click", function(){mark(true)});
document.getElementsByClassName("plus-mark-unread")[0].addEventListener("click", function(){mark(false)});
}
function startReading(){
let manga_id = getSubPage(2).replace("#", "");
let chapter_id = "";
let api_url = api_base_url + "manga/" + manga_id + "/chapters";
async function getapi(url) {
let response = await fetch(url);
var data = await response.json();
var data2 = "";
var length = data.data.chapters.length;
for(let i = 0; i < length; i++){
if (await data.data.chapters[length-i-1].language == "gb"){
data2 = await data.data.chapters[length-i-1].id;
break;
}
}
return data2
}
getapi(api_url).then(x => {
chapter_id = x;
let link = "https://mangadex.org/chapter/" + chapter_id;
let link2 = "";
try{
let container = document.getElementsByClassName("chapter-container ")[0];
for(let i = 1; i < container.children.length; i++){
if(!container.children[i].children[0].children[0].children[0].children[0].classList.contains("grey")){
link2 = container.children[i-1].children[0].children[0].children[1].children[0].attributes.href.value;
}
}
}
catch(e){
link2 = "#";
}
let mark_button = '