FA Webcomic Autoloader

Gives you the option to load all the subsequent comic pages on a FurAffinity comic page automatically. Even for pages without given Links

目前為 2025-01-12 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name FA Webcomic Autoloader
  3. // @namespace Violentmonkey Scripts
  4. // @match *://*.furaffinity.net/*
  5. // @require https://update.gf.qytechs.cn/scripts/475041/1267274/Furaffinity-Custom-Settings.js
  6. // @require https://update.gf.qytechs.cn/scripts/483952/1478384/Furaffinity-Request-Helper.js
  7. // @require https://update.gf.qytechs.cn/scripts/485153/1316289/Furaffinity-Loading-Animations.js
  8. // @require https://update.gf.qytechs.cn/scripts/485827/1318253/Furaffinity-Match-List.js
  9. // @grant none
  10. // @version 2.0.7
  11. // @author Midori Dragon
  12. // @description Gives you the option to load all the subsequent comic pages on a FurAffinity comic page automatically. Even for pages without given Links
  13. // @icon https://www.furaffinity.net/themes/beta/img/banners/fa_logo.png?v2
  14. // @homepageURL https://gf.qytechs.cn/de/scripts/457759-furaffinity-webcomic-autoloader-2-0
  15. // @supportURL https://gf.qytechs.cn/de/scripts/457759-furaffinity-webcomic-autoloader-2-0/feedback
  16. // @license MIT
  17. // ==/UserScript==
  18.  
  19. // jshint esversion: 8
  20.  
  21. CustomSettings.name = "Extension Settings";
  22. CustomSettings.provider = "Midori's Script Settings";
  23. CustomSettings.headerName = `${GM_info.script.name} Settings`;
  24. const showSearchButtonSetting = CustomSettings.newSetting("Simular Search Button", "Sets wether the search for simular Pages button is show.", SettingTypes.Boolean, "Show Search Button", true);
  25. const loadingSpinSpeedSetting = CustomSettings.newSetting("Loading Animation", "Sets the duration that the loading animation takes for a full rotation in milliseconds.", SettingTypes.Number, "", 600);
  26. const backwardSearchSetting = CustomSettings.newSetting("Backward Search", "Sets the amount of simular pages to search backward. (More Pages take longer)", SettingTypes.Number, "Backward Search Amount", 2);
  27. CustomSettings.loadSettings();
  28.  
  29. const matchList = new MatchList(CustomSettings);
  30. matchList.matches = ['net/view'];
  31. if (!matchList.hasMatch())
  32. return;
  33.  
  34. const nextText = "next";
  35. const prevText = "prev";
  36. const firstText = "first";
  37.  
  38. const requestHelper = new FARequestHelper(2);
  39.  
  40. let lightboxPresent = false;
  41. let currLightboxNo = -1;
  42. let imgCount = 1;
  43.  
  44. let rootSubmissionImg = document.getElementById("submissionImg");
  45. rootSubmissionImg.setAttribute('imgno', 0);
  46. rootSubmissionImg.setAttribute('rootSubmissionImg', true);
  47. rootSubmissionImg.addEventListener('click', submissionImgOnClick);
  48. let openedSids = [getIdFromUrl(window.location.toString())];
  49.  
  50. function CheckTags(element) {
  51. var _a;
  52. if (!("1" === document.body.getAttribute("data-user-logged-in"))) return;
  53. const tagsHideMissingTags = "1" === document.body.getAttribute("data-tag-blocklist-hide-tagless"), tags = null === (_a = element.getAttribute("data-tags")) || void 0 === _a ? void 0 : _a.trim().split(/\s+/);
  54. let blockReason = "";
  55. if (null != tags && tags.length > 0 && "" !== tags[0]) {
  56. const blockedTags = function getBannedTags(tags) {
  57. var _a;
  58. const tagsBlocklist = null !== (_a = document.body.getAttribute("data-tag-blocklist")) && void 0 !== _a ? _a : [];
  59. let bTags = [];
  60. if (null == tags || 0 === tags.length) return [];
  61. for (const tag of tags) for (const blockedTag of tagsBlocklist) tag === blockedTag && bTags.push(blockedTag);
  62. return [ ...new Set(bTags) ];
  63. }(tags);
  64. if (blockedTags.length <= 0) setBlockedState(element, !1); else {
  65. setBlockedState(element, !0), blockReason = "Blocked tags:\n";
  66. for (const tag of blockedTags) blockReason += "• " + tag + "\n";
  67. }
  68. } else setBlockedState(element, tagsHideMissingTags), tagsHideMissingTags && (blockReason = "Content is missing tags.");
  69. "" !== blockReason && "submissionImg" !== element.id && element.setAttribute("title", blockReason);
  70. }
  71. function setBlockedState(element, isBlocked) {
  72. element.classList[isBlocked ? "add" : "remove"]("blocked-content");
  73. }
  74. function CheckTagsAll(doc) {
  75. if (null == doc) return;
  76. doc.querySelectorAll("img[data-tags]").forEach((element => CheckTags(element)));
  77. }
  78.  
  79. createLoaderButton();
  80.  
  81. function createLoaderButton() {
  82. const hasSecondPage = getNavigationIds(document).next;
  83.  
  84. const autoLoaderButton = document.createElement('button');
  85. autoLoaderButton.id = "autoloaderbutton";
  86. autoLoaderButton.className = "button standard mobile-fix";
  87. autoLoaderButton.type = "button";
  88. autoLoaderButton.style.marginTop = "10px";
  89. autoLoaderButton.style.marginBottom = "20px";
  90.  
  91. if (hasSecondPage) {
  92. autoLoaderButton.textContent = "Enable Comic Autoloader";
  93. autoLoaderButton.onclick = startAutoloader;
  94. insertAfter(autoLoaderButton, rootSubmissionImg);
  95. insertBreakBefore(autoLoaderButton, rootSubmissionImg);
  96. } else if (showSearchButtonSetting.value) {
  97. autoLoaderButton.textContent = "Search for simular Pages";
  98. autoLoaderButton.onclick = startSimularSearch;
  99. insertAfter(autoLoaderButton, rootSubmissionImg);
  100. insertBreakBefore(autoLoaderButton, rootSubmissionImg);
  101. }
  102. }
  103.  
  104. async function startAutoloader() {
  105. const autoLoaderButton = document.getElementById("autoloaderbutton");
  106. autoLoaderButton.parentNode.removeChild(autoLoaderButton);
  107.  
  108. let sids = getNavigationIds(document);
  109. let lastSubmissionImg = document.getElementById("submissionImg");
  110. while (sids.next) {
  111. const newDoc = await loadPage(sids.next, lastSubmissionImg);
  112. lastSubmissionImg = document.getElementById("columnpage").querySelector('img[imgno="' + (openedSids.length - 1) + '"]');
  113. sids = getNavigationIds(newDoc);
  114. }
  115. CheckTagsAll(document);
  116. }
  117.  
  118. async function startSimularSearch() {
  119. const autoLoaderButton = document.getElementById("autoloaderbutton");
  120. const spinner = new LoadingTextSpinner(autoLoaderButton);
  121. spinner.delay = loadingSpinSpeedSetting.value;
  122. spinner.visible = true;
  123. const result = await searchAllSimularPages();
  124. spinner.visible = false;
  125. if (result)
  126. autoLoaderButton.parentNode.removeChild(autoLoaderButton);
  127. else
  128. autoLoaderButton.textContent = "Nothing found... Search again";
  129. CheckTagsAll(document);
  130. }
  131.  
  132. function getNavigationIds(doc) {
  133. let nextSid;
  134. let prevSid;
  135. let startSid;
  136. if (doc) {
  137. const links = doc.querySelectorAll('a[href]:not([class*="button standard mobile-fix"]), :not([class])');
  138. for (const elem of links) {
  139. const navText = elem.textContent.toLowerCase();
  140. if (navText.length > 12)
  141. continue;
  142. if (navText.includes(nextText))
  143. nextSid = getIdFromUrl(elem.href);
  144. if (navText.includes(prevText))
  145. prevSid = getIdFromUrl(elem.href);
  146. if (navText.includes(firstText))
  147. startSid = getIdFromUrl(elem.href);
  148. }
  149. }
  150. const sids = { next: nextSid, prev: prevSid, start: startSid };
  151. return sids;
  152. }
  153.  
  154. async function loadPage(sid, lastSubmissionImg) {
  155. if (sid && !openedSids.includes(sid)) {
  156. const submissionPage = await requestHelper.SubmissionRequests.getSubmissionPage(sid);
  157. if (submissionPage && submissionPage.getElementById("submissionImg")) {
  158. openedSids.push(sid);
  159. const submissionImg = submissionPage.getElementById("submissionImg");
  160. submissionImg.setAttribute('imgno', openedSids.length - 1);
  161. submissionImg.addEventListener('click', submissionImgOnClick);
  162.  
  163. insertAfter(submissionImg, lastSubmissionImg);
  164. insertBreakBefore(submissionImg);
  165. insertBreakBefore(submissionImg);
  166.  
  167. return submissionPage;
  168. }
  169. }
  170. }
  171.  
  172. async function loadPageBefore(sid, lastSubmissionImg) {
  173. if (sid && !openedSids.includes(sid)) {
  174. const submissionPage = await requestHelper.SubmissionRequests.getSubmissionPage(sid);
  175. if (submissionPage && submissionPage.getElementById("submissionImg")) {
  176. openedSids.push(sid);
  177. const submissionImg = submissionPage.getElementById("submissionImg");
  178. submissionImg.setAttribute('imgno', openedSids.length - 1);
  179. submissionImg.addEventListener('click', submissionImgOnClick);
  180.  
  181. insertBefore(submissionImg, lastSubmissionImg);
  182. insertBreakBefore(submissionImg);
  183. insertBreakBefore(submissionImg);
  184.  
  185. return submissionPage;
  186. }
  187. }
  188. }
  189.  
  190. async function searchAllSimularPages() {
  191. const submissionPage = document.getElementById("submission_page");
  192. const container = submissionPage.querySelector('div[class="submission-id-sub-container"]');
  193. let currTitle = container.querySelector('div[class="submission-title"]').querySelector('p').textContent;
  194. const isFirst = currTitle.includes("1");
  195. currTitle = generalizeString(currTitle, true, true, true, true, true);
  196. const author = container.querySelector('a[href]');
  197.  
  198. let user = author.href;
  199. if (user.endsWith("/"))
  200. user = user.substring(0, user.length - 1);
  201. user = user.substring(user.lastIndexOf("/") + 1);
  202.  
  203. const sid = getIdFromUrl(window.location.toString());
  204.  
  205. const galleryPages = await requestHelper.UserRequests.GalleryRequests.Gallery.getFiguresTillId(user, sid);
  206. const simularFigures = [];
  207. let currPage = 1;
  208. for (const figures of galleryPages) {
  209. for (const figure of figures) {
  210. const title = getTitleFromFigureGeneralized(figure);
  211. if (title != "" && (title.includes(currTitle) || currTitle.includes(title))) {
  212. if (figure.id.toString().replace('sid-', '') != sid) {
  213. simularFigures.push(figure);
  214. }
  215. }
  216. }
  217. currPage++;
  218. }
  219.  
  220. const simularFiguresBefore = [];
  221. if (isFirst === false && backwardSearchSetting.value !== 0) {
  222. const galleryPagesBefore = await requestHelper.UserRequests.GalleryRequests.Gallery.getFiguresSinceIdTillPage(user, sid, currPage + backwardSearchSetting.value);
  223. if (galleryPagesBefore) {
  224. for (const figures of galleryPagesBefore) {
  225. for (const figure of figures) {
  226. const title = getTitleFromFigureGeneralized(figure);
  227. if (title != "" && (title.includes(currTitle) || currTitle.includes(title))) {
  228. if (figure.id.toString().replace('sid-', '') != sid) {
  229. simularFiguresBefore.push(figure);
  230. }
  231. }
  232. }
  233. }
  234. }
  235. }
  236.  
  237. if (simularFigures.length === 0 && simularFiguresBefore.length === 0)
  238. return false;
  239.  
  240. simularFigures.reverse();
  241. const simularSids = simularFigures.map(figure => figure.id.toString().replace('sid-', ''));
  242.  
  243. simularFiguresBefore.reverse();
  244. const simularSidsBefore = simularFiguresBefore.map(figure => figure.id.toString().replace('sid-', ''));
  245.  
  246. openedSids = [];
  247. let lastSubmissionImg = document.getElementById("submissionImg");
  248. if (simularSidsBefore.length !== 0) {
  249. rootSubmissionImg.setAttribute('imgno', -1);
  250. await loadPageBefore(simularSidsBefore[0], lastSubmissionImg);
  251. for (const sid of simularSidsBefore) {
  252. await loadPage(sid, lastSubmissionImg);
  253. lastSubmissionImg = [...document.querySelectorAll('img[imgno="' + (openedSids.length - 1) + '"]')].pop();
  254. }
  255. lastSubmissionImg = document.querySelector('img[rootSubmissionImg="true"]');
  256. lastSubmissionImg.setAttribute('imgno', openedSids.length);
  257. insertBreakBefore(lastSubmissionImg);
  258. insertBreakBefore(lastSubmissionImg);
  259. }
  260. openedSids.push(getIdFromUrl(window.location.toString()));
  261.  
  262. for (const sid of simularSids) {
  263. await loadPage(sid, lastSubmissionImg);
  264. lastSubmissionImg = [...document.querySelectorAll('img[imgno="' + (openedSids.length - 1) + '"]')].pop();
  265. }
  266. return true;
  267. }
  268.  
  269. function getTitleFromFigure(figure) {
  270. const figcaption = figure.querySelector('figcaption');
  271. let title = figcaption.querySelector('a[href]').textContent;
  272. return title;
  273. }
  274.  
  275. function getTitleFromFigureGeneralized(figure) {
  276. const figcaption = figure.querySelector('figcaption');
  277. let title = figcaption.querySelector('a[href]').textContent;
  278. title = generalizeString(title, true, true, true, true, true);
  279. return title;
  280. }
  281.  
  282. function getIdFromUrl(url) {
  283. try {
  284. const firstNumberIndex = url.search(/\d/);
  285. const lastNumberIndex = url.lastIndexOf(url.match(/\d(?=\D*$)/));
  286. const id = url.substring(firstNumberIndex, lastNumberIndex + 1);
  287. return id;
  288. } catch {
  289. return;
  290. }
  291. }
  292.  
  293. function submissionImgOnClick(event) {
  294. const img = event.target;
  295. if (document.querySelectorAll('img[imgno]').length > 1) {
  296. showLightBox(img);
  297. }
  298. event.preventDefault();
  299. }
  300.  
  301. function showLightBox(img) {
  302. const lightbox = document.createElement('div');
  303. lightbox.className = 'lightbox lightbox-submission';
  304. lightbox.onclick = () => {
  305. document.body.removeChild(lightbox);
  306. lightboxPresent = false;
  307. currLightboxNo = -1;
  308. window.removeEventListener('keydown', handleArrowKeys);
  309. };
  310. const lightboxImg = img.cloneNode(false);
  311. lightbox.appendChild(lightboxImg);
  312. document.body.appendChild(lightbox);
  313. lightboxPresent = true;
  314. currLightboxNo = +img.getAttribute('imgno');
  315. window.addEventListener('keydown', handleArrowKeys);
  316. }
  317.  
  318. function navigateLightboxLeft() {
  319. if (currLightboxNo > 0) {
  320. currLightboxNo--;
  321. const lightbox = document.body.querySelector('div[class="lightbox lightbox-submission"]');
  322. const lightboxImg = lightbox.querySelector('img');
  323. const nextImg = document.querySelector('img[imgno="' + currLightboxNo + '"]');
  324. lightboxImg.src = nextImg.src;
  325. }
  326. }
  327.  
  328. function navigateLightboxRight() {
  329. if (currLightboxNo < openedSids.length - 1) {
  330. currLightboxNo++;
  331. const lightbox = document.body.querySelector('div[class="lightbox lightbox-submission"]');
  332. const lightboxImg = lightbox.querySelector('img');
  333. const nextImg = document.querySelector('img[imgno="' + currLightboxNo + '"]');
  334. lightboxImg.src = nextImg.src;
  335. }
  336. }
  337.  
  338. function handleArrowKeys(event) {
  339. if (event.keyCode === 37) { // left arrow
  340. navigateLightboxLeft();
  341. } else if (event.keyCode === 38) { // up arrow
  342. navigateLightboxLeft();
  343. } else if (event.keyCode === 39) { // right arrow
  344. navigateLightboxRight();
  345. } else if (event.keyCode === 40) { // down arrow
  346. navigateLightboxRight();
  347. }
  348. event.preventDefault();
  349. }
  350.  
  351. function insertAfter(newNode, referenceNode) {
  352. referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  353. }
  354.  
  355. function insertBefore(newNode, referenceNode) {
  356. referenceNode.parentNode.insertBefore(newNode, referenceNode);
  357. }
  358.  
  359. function insertBreakAfter(referenceNode) {
  360. insertAfter(document.createElement("br"), referenceNode);
  361. }
  362.  
  363. function insertBreakBefore(referenceNode) {
  364. referenceNode.parentNode.insertBefore(document.createElement("br"), referenceNode);
  365. }
  366.  
  367. function generalizeString(inputString, textToNumbers, removeSpecialChars, removeNumbers, removeSpaces, removeRoman) {
  368. let outputString = inputString.toLowerCase();
  369.  
  370. if (removeRoman) {
  371. const roman = ["i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix", "x", "xi", "xii", "xiii", "xiv", "xv", "xvi", "xvii", "xviii", "xix", "xx"]; //Checks only up to 20
  372. outputString = outputString.replace(new RegExp(`(?:^|[^a-zA-Z])(${roman.join("|")})(?:[^a-zA-Z]|$)`, "g"), "");
  373. }
  374.  
  375. if (textToNumbers) {
  376. const numbers = { zero: 0, one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10, eleven: 11, twelve: 12, thirteen: 13, fourteen: 14, fifteen: 15, sixteen: 16, seventeen: 17, eighteen: 18, nineteen: 19, twenty: 20, thirty: 30, forty: 40, fifty: 50, sixty: 60, seventy: 70, eighty: 80, ninety: 90, hundred: 100 };
  377. outputString = outputString.replace(new RegExp(Object.keys(numbers).join("|"), "gi"), match => numbers[match.toLowerCase()]);
  378. }
  379.  
  380. if (removeSpecialChars)
  381. outputString = outputString.replace(/[^a-zA-Z0-9 ]/g, "");
  382.  
  383. if (removeNumbers)
  384. outputString = outputString.replace(/[^a-zA-Z ]/g, "");
  385.  
  386. if (removeSpaces)
  387. outputString = outputString.replace(/\s/g, "");
  388.  
  389. return outputString;
  390. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址