// ==UserScript==
// @name Royal Road Download Button
// @license MIT
// @namespace rtonne
// @match https://www.royalroad.com/fiction/*
// @exclude https://www.royalroad.com/fiction/*/chapter/*
// @grant none
// @version 3.1
// @author Rtonne
// @description Adds "Download All Chapters" button to Royal Road fictions
// @require https://cdn.jsdelivr.net/npm/[email protected]
// @require https://cdn.jsdelivr.net/npm/[email protected]
// @run-at document-end
// ==/UserScript==
const customStyle = '<style>.portlet-body p,p{margin-top:0}body{background:#181818;font-family:Open Sans, open-sans, Helvetica Neue, Helvetica, Roboto, Arial, sans-serif;line-height:1.42857143;font-size:16px;margin:0}.font-white{color:#fff !important}.portlet{background:#131313;border:1px solid hsla(0, 0%, 100%, 0.1);color:hsla(0, 0%, 100%, 0.8);padding:1em 20px 0;margin:10px 0;display:flex;flex-direction:column}.author-note-portlet{background:#393939;color:hsla(0, 0%, 100%, 0.8);border:0;padding:0 10px 10px;margin:0 0 1em}.portlet-title{border-bottom:0;margin-bottom:10px;min-height:41px;padding:0;margin-left:15px}.caption{padding:16px 0 2px;display:inline-block;float:left;font-size:18px;line-height:18px}.uppercase{text-transform:uppercase !important}.bold{font-weight:700 !important}a{color:#337ab7;text-shadow:none;text-decoration:none}.portlet-body{padding:10px 15px}p{margin-bottom:1em}.col-md-5{min-height:1px;background:#2a3642;margin-left:-15px;margin-right:-15px;padding:10px}.text-center{text-align:center}.container{margin-left:auto;margin-right:auto;padding-left:15px;padding-right:15px;width:"100%"}.col-md-5 > *,.col-md-5 > * > *{font-weight:300;margin:10px 0}table{background:#004b7a;border:none;border-collapse:separate;border-spacing:2px;box-shadow:1px 1px 1px rgba(0, 0, 0, 0.75);margin:10px auto;width:90%}table td{background:rgba(0, 0, 0, 0.1);border:1px solid hsla(0, 0%, 100%, 0.25) !important;color:#ccc;margin:3px;padding:5px}.spoiler,.spoiler-new{max-height:20px;padding-top:100px;overflow-y:scroll;border:1px solid hsla(0, 0%, 100%, 0.5)}.spoiler-new:before,.spoiler:before{content:"Spoiler ahead:"}.btn-primary{box-shadow:none;outline:none;line-height:1.44;background-color:#337ab7;color:#fff;padding:6px 0;text-align:center;display:inline-block;font-size:14px;font-weight:400;border:1px solid #2e6da4}.btn-primary[disabled]{cursor:not-allowed;opacity:0.65}.col-xs-12{width:100%}.col-xs-6{width:50%;position:relative;float:left}.visible-xs,.visible-xs-block{display:none}.col-xs-4{width:33.33333333%;margin:0}.row{display:flex;margin-bottom:1em}@media (min-width: 1200px){.container{width:1170px}.col-lg-3{width:25%}.col-lg-offset-6{margin-left:50%}}@media (min-width: 992px){.container{width:970px}.col-md-4{width:33.33333333%}.col-md-offset-4{margin-left:33.33333333%}}</style>';
const button = document.createElement("a");
button.className = "button-icon-large";
const buttonStyle = getComputedStyle(document.querySelector("a.button-icon-large"));
const progressBar = document.createElement("div");
progressBar.style.position = "absolute";
progressBar.style.top = `calc(${buttonStyle.height} - ${buttonStyle.borderBottomWidth})`;
progressBar.style.right = "0";
progressBar.style.height = buttonStyle.borderBottomWidth;
progressBar.style.background = getComputedStyle(document.querySelector("a.btn-primary")).backgroundColor;
progressBar.style.width = "0";
progressBar.className = "RRScraperProgressBar";
button.appendChild(progressBar);
const i = document.createElement("i");
i.className = "fa fa-download";
button.appendChild(i);
const span = document.createElement("span");
span.innerText = "Download All Chapters";
span.className = "center";
button.appendChild(span);
const defaultButtonRows = document.querySelectorAll("div.row.reduced-gutter");
defaultButtonRows.forEach((defaultButtonRow) => {
const buttonClone = button.cloneNode(true);
buttonClone.onclick = () => {
download();
};
defaultButtonRow.insertAdjacentElement("afterend", buttonClone);
});
async function download() {
document.querySelectorAll("div.RRScraperProgressBar").forEach(element => {
element.style.width = "100%";
});
const parser = new DOMParser();
const zip = new JSZip();
const urlSplit = window.location.href.split("/");
const fictionName = urlSplit[urlSplit.length - 1];
var newHtml = await fetch(window.location.href, {credentials: 'omit'})
.then(response => response.text())
.then(text => parser.parseFromString(text, "text/html"));
const chapterUrls = [...newHtml.querySelectorAll("tr.chapter-row")].map(element => {
return element.getAttribute("data-url");
});
const chapterCount = chapterUrls.length;
const chapterCountLength = chapterCount.toString().length;
const fillZeros = "0".repeat(chapterCountLength);
// timeoutLoop for the progress bar to work
let index = 0;
async function timeoutLoop() {
let chapterUrl = chapterUrls[index];
newHtml = await fetch("https://www.royalroad.com" + chapterUrl, {credentials: 'omit'})
.then(response => response.text())
.then(text => parser.parseFromString(text, "text/html"));
let chapterHeader = newHtml.querySelector("div.fic-header > div > div.col-lg-6");
chapterHeader.querySelectorAll("a").forEach(element => {
element.setAttribute("href", `https://www.royalroad.com${element.getAttribute("href")}`);
});
let chapter = customStyle + '\n<div class="container">' + chapterHeader.outerHTML;
chapter += '\n<div class="portlet">';
[...newHtml.querySelector("div.chapter-content").parentNode.children].forEach(element => {
if (element.classList.contains("chapter-content") || element.classList.contains("author-note-portlet")) {
chapter += "\n" + element.outerHTML;
} else if (element.classList.contains("nav-buttons") || element.classList.contains("margin-bottom-10")) {
element.querySelectorAll('a').forEach(element2 => {
if (element2.innerText.includes("Index")) {
element2.setAttribute("href", ".");
return;
}
let adjFilledIndex = "";
if (element2.innerText.includes("Previous")) {
adjFilledIndex = (fillZeros + index).slice(chapterCountLength * -1);
} else if (element2.innerText.includes("Next")) {
adjFilledIndex = (fillZeros + (index + 2)).slice(chapterCountLength * -1);
}
let adjChapterUrlSplit = element2.getAttribute("href").split("/");
let adjChapterName = adjChapterUrlSplit[adjChapterUrlSplit.length - 1];
element2.setAttribute("href", `${adjFilledIndex}_${adjChapterName}.html`);
})
chapter += "\n" + element.outerHTML;
}
})
chapter += '\n</div></div>';
let chapterUrlSplit = chapterUrl.split("/");
let chapterName = chapterUrlSplit[chapterUrlSplit.length - 1];
let filledIndex = (fillZeros + (index + 1)).slice(chapterCountLength * -1);
zip.file(`${fictionName}/${filledIndex}_${chapterName}.html`, chapter);
document.querySelectorAll("div.RRScraperProgressBar").forEach(element => {
element.style.width = `${((chapterCount - index - 1) / chapterCount) * 100}%`;
});
if (++index < chapterCount) {
setTimeout(timeoutLoop, 0);
} else {
zip.generateAsync({
type: "blob",
compression: "DEFLATE",
compressionOptions: {
level: 9
}
}).then((blob) => {
saveAs(blob, fictionName + ".zip");
});
}
}
setTimeout(timeoutLoop, 0);
}