// ==UserScript==
// @namespace https://greasyfork.dpdns.org/users/294014-useyourname
// @name MangaDex mark as read
// @description Slide the button down to mark everything below as read
// @copyright 2019, UseYourName
// @license Beerware
// @version 4
// @match https://mangadex.org/title/*
// @match https://mangadex.org/manga*
// @require https://code.jquery.com/ui/1.12.1/jquery-ui.min.js
// ==/UserScript==
// If previous pages should be marked read
const MARK_PAGES = true;
// Number of simultaneous requests for marking read
const MARK_CONCURRENT = 20;
$(document).ready(function() {
// Timeout 1ms so it's run after site stuff
setTimeout(function() {
// Unbind default click handlers for marking (un)read
$(".chapter_mark_read_button, .chapter_mark_unread_button").unbind("click");
// One handler for both to make it easy to process repeated toggles
$(".chapter_mark_read_button, .chapter_mark_unread_button").click(function(event){
queueChapter($(this).attr('data-id'), ($(this).hasClass("chapter_mark_read_button") ? "read" : "unread"));
event.preventDefault();
});
}, 1);
});
// Make mark read/unread draggable and disable by default
$(".chapter_mark_read_button, .chapter_mark_unread_button").draggable({
helper: function(event) {
// Make a thing to drag
var targetDiv = $('<div id="div_draggable"><span class="fas fa-eye-slash fa-fw " aria-hidden="true"></span></div>');
targetDiv
.css({
'top': $(this).offset().top,
'left': $(this).offset().left,
'width': $(this).width(),
'height': $(this).height(),
'position': 'absolute'
});
// div used to limit movement, height multiplier is pretty much randomly picked
var containmentDiv = $('<div id="div_containment"></div>');
containmentDiv
.css({
'display': "hidden",
'top': $(this).offset().top,
'left': $(this).offset().left,
'width': $(this).width(),
'height': $(this).height() * 2.5,
'position': 'absolute'
});
$("body").append(containmentDiv);
// div used to check if dragged all the way down, should be possible to calculate 'top' based on previous multiplier but eh
var droppableDiv = $('<div id="div_droppable"></div>');
droppableDiv
.css({
'display': "hidden",
'top': $(this).offset().top + ($(this).height() * 1.9),
'left': $(this).offset().left,
'width': $(this).width(),
'height': $(this).height(),
'position': 'absolute'
});
droppableDiv.droppable({
// visual cue for when fully dragged
over: function(event, ui) {
ui.helper.children(".fas").switchClass("fa-eye-slash", "fa-eye");
},
out: function(event, ui) {
ui.helper.children(".fas").switchClass("fa-eye", "fa-eye-slash");
},
drop: function(event, ui) {
// trigger all the buttons below this, and this one
ui.draggable.trigger("click");
ui.draggable.closest("div[class='row no-gutters']").nextAll("div[class='row no-gutters']").find(".chapter_mark_read_button").trigger("click");
if(MARK_PAGES) {
markPagesRead(ui.draggable.attr('data-id'));
}
}
});
$("body").append(droppableDiv);
return targetDiv;
},
axis: 'y',
containment: "#div_containment",
appendTo: "body",
revert: true,
disabled: true,
start: function(event, ui) {
$(this).hide();
},
stop: function(event, ui) {
$("#div_droppable").remove();
$("#div_containment").remove();
$(this).show();
}
});
// Enable for the mark read buttons
$(".chapter_mark_read_button").draggable({disabled:false});
// Store the id of the chapter which triggered updates to stop repeated marking of all pages
var chapterDragged = 0;
function markPagesRead(chapter_id) {
if(chapterDragged) return;
chapterDragged = chapter_id;
var titleID, pageCurrent, pageTotal;
titleID = Number($("link[rel='canonical']").attr("href").split("/")[4]);
pageCurrent = Number(location.pathname.split("/")[5]);
pageTotal = Number(($(".page-link").last().attr('href') || "").split("/")[5]);
if(!pageCurrent) pageCurrent = 1;
if(!pageTotal) pageTotal = 1;
if(pageCurrent < pageTotal) {
// CSS for rotating eye while processing
var myStylesheet = document.createElement('style');
myStylesheet.type = "text/css";
if(CSS && CSS.supports && CSS.supports('animation: name')) {
myStylesheet.innerHTML =
"@keyframes eyerotate { to { transform: rotate(360deg); } }\n" +
"\n" +
".eyerotate {\n" +
" animation: eyerotate 1s linear infinite;\n" +
"}";
} else {
myStylesheet.innerHTML =
"@keyframes eyerotate { to { transform: rotate(360deg); } }\n" +
"@-webkit-keyframes eyerotate { to { -webkit-transform: rotate(360deg); } }\n" +
"\n" +
".eyerotate {\n" +
" animation: eyerotate 1s linear infinite;\n" +
" -webkit-animation: eyerotate 1s linear infinite;\n" +
"}";
}
document.head.appendChild(myStylesheet);
for (let i = 1; i + pageCurrent <= pageTotal; i++) {
$.get(`/title/${titleID}/_/chapters/${pageCurrent+i}/`, function(data) {
// Ugly, ugly processing to skip loading img/script elements
var start = data.indexOf('<div class="chapter-container ">');
var end = data.indexOf("<p class='mt-3 text-center'>");
if(start != -1 && end != -1) {
$(data.slice(start, end)).find('.chapter_mark_read_button').each(function(index) {
queueChapter($(this).attr('data-id'));
});
}
}, "html" );
}
}
}
var queueChapter = (function () {
var chapterQueue = [];
var chapterMarking = 0;
var chapterSpinning = false;
// Spin eye to indicate requests in progress
function spinChapter(toggle=true) {
// If chapterDragged isn't set then queued requests are from this page, no need for a visual inidicator
if(!chapterDragged) return;
if(toggle) {
$("#marker_"+chapterDragged).children(".fas").addClass("eyerotate");
} else if (CSS && CSS.supports && CSS.supports('animation: name')) {
$("#marker_"+chapterDragged).children(".fas").on('animationiteration', function () {
$(this).off('animationiteration').removeClass('eyerotate');
});
} else {
$("#marker_"+chapterDragged).children(".fas").on('animationiteration webkitAnimationIteration', function () {
$(this).off('animationiteration webkitAnimationIteration').removeClass('eyerotate');
});
}
}
// Should perhaps requeue on failure, but then I'd need to store retrys and limit them
function updateChapter(chapter_id, mark) {
$.ajax({
...(!!mark && {success: function () {
if(mark == "unread") {
$("#marker_"+chapter_id)
.removeClass("chapter_mark_unread_button")
.addClass("grey chapter_mark_read_button")
.prop("title", "Mark read")
.draggable({disabled:false})
.children(".fas").switchClass("fa-eye", "fa-eye-slash");
} else {
$("#marker_"+chapter_id)
.removeClass("grey chapter_mark_read_button")
.addClass("chapter_mark_unread_button")
.prop("title", "Mark unread")
.draggable({disabled:true})
.children(".fas").switchClass("fa-eye-slash", "fa-eye");
}
}
}),
url: "/ajax/actions.ajax.php",
data: {
function: (mark === "unread" ? "chapter_mark_unread" : "chapter_mark_read"),
id: chapter_id
},
complete: function() {
if(chapterQueue.length) {
updateChapter(...chapterQueue.shift());
} else {
chapterMarking--;
if(!chapterMarking && chapterSpinning) {
chapterSpinning = false;
spinChapter(false);
}
}
},
cache: false,
contentType: false
});
}
return function (chapter_id, mark) {
if(chapterMarking < MARK_CONCURRENT) {
chapterMarking++;
updateChapter(chapter_id, mark);
} else {
chapterQueue.push([chapter_id, mark]);
if(!chapterSpinning) {
chapterSpinning = true;
spinChapter(true);
}
}
}
})();