您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds "Download All Chapters" button to Royal Road fictions
当前为
// ==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.4 // @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 button = createDownloadAllButton(); // Add a clone of the download all button to both button lists for different screen widths const defaultButtonRows = document.querySelectorAll("div.row.reduced-gutter"); defaultButtonRows.forEach((defaultButtonRow) => { const buttonClone = button.cloneNode(true); buttonClone.onclick = () => { downloadAll(); }; defaultButtonRow.insertAdjacentElement("afterend", buttonClone); }); async function downloadAll() { // Start progress bar 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]; // Get fiction page again to get full chapter list 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; // 0 required so all chapter numbers use the same amount of characters const fillZeros = "0".repeat(chapterCountLength); // timeoutLoop for the progress bar to work let index = 0; async function timeoutLoop() { let chapterUrl = chapterUrls[index]; // Get chapter newHtml = await fetch("https://www.royalroad.com" + chapterUrl, { credentials: "omit", }) .then((response) => response.text()) .then((text) => parser.parseFromString(text, "text/html")); if ( newHtml.body.firstChild.tagName === "PRE" && newHtml.body.firstChild.innerText === "Slow down!" ) { // When pages are loaded too fast RoyalRoad may tell you to slow down // So we retry it if it does print("Slow down!"); setTimeout(timeoutLoop, 0); return; } // Edit the header links so they work offline 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 = getCustomHeader() + 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") ) { // Add chapter content and author notes chapter += "\n" + element.outerHTML; } else if ( element.classList.contains("nav-buttons") || element.classList.contains("margin-bottom-10") ) { // Add prev/next/index buttons and make them work offline 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 += getCustomFooter(); 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); // Change progress bar document.querySelectorAll("div.RRScraperProgressBar").forEach((element) => { element.style.width = `${ ((chapterCount - index - 1) / chapterCount) * 100 }%`; }); if (++index < chapterCount) { // If there are chapters left, fetch them setTimeout(timeoutLoop, 0); } else { // If all chapters have been fetched, zip them and download them zip .generateAsync({ type: "blob", compression: "DEFLATE", compressionOptions: { level: 9, }, }) .then((blob) => { saveAs(blob, fictionName + ".zip"); }); } } setTimeout(timeoutLoop, 0); } function createDownloadAllButton() { 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); return button; } function getCustomHeader() { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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: 992px) { .container { width: 970px; } .col-md-4 { width: 33.33333333%; } .col-md-offset-4 { margin-left: 33.33333333%; } } @media (min-width: 1200px) { .container { width: 1170px; } .col-lg-3 { width: 25%; } .col-lg-offset-6 { margin-left: 50%; } } </style> </head> <body> <div class="container">`; } function getCustomFooter() { return "</div></div></body></html>"; }