Toonily Manga Loader

Forces Toonily to load all manga images at once and dynamically loads them into a single page strip with a stats window.

  1. // ==UserScript==
  2. // @name Toonily Manga Loader
  3. // @namespace github.com/longkidkoolstar
  4. // @version 1.1
  5. // @description Forces Toonily to load all manga images at once and dynamically loads them into a single page strip with a stats window.
  6. // @author longkidkoolstar
  7. // @match https://toonily.com/webtoon/*
  8. // @require https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/js/all.min.js
  9. // @grant GM.setValue
  10. // @grant GM.getValue
  11. // @grant GM.deleteValue
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. let imageUrls = [];
  19. let loadedImages = 0;
  20. let totalImages = 0;
  21. let reloadMode = false;
  22.  
  23. // Function to simulate click on the "Load All Images" switch
  24. function enableLoadAllImages() {
  25. const loadAllImagesButton = document.querySelector('#btn-lazyload-controller');
  26. if (loadAllImagesButton && loadAllImagesButton.querySelector('.fa-toggle-off')) {
  27. loadAllImagesButton.click(); // Simulate clicking to enable "Load All Images"
  28. console.log("Forcing 'Load All Images'...");
  29. }
  30. }
  31.  
  32. // Hooking into XMLHttpRequest to capture all image URLs from AJAX responses
  33. function interceptAjaxRequests() {
  34. const originalOpen = XMLHttpRequest.prototype.open;
  35.  
  36. XMLHttpRequest.prototype.open = function(method, url, ...args) {
  37. this.addEventListener('load', function() {
  38. if (url.includes('/chapters/manga')) {
  39. const parser = new DOMParser();
  40. const doc = parser.parseFromString(this.responseText, 'text/html');
  41. const imgs = doc.querySelectorAll('img.wp-manga-chapter-img');
  42.  
  43. imgs.forEach(img => {
  44. const imgUrl = img.src.trim();
  45. if (!imageUrls.includes(imgUrl)) {
  46. imageUrls.push(imgUrl);
  47. }
  48. });
  49. }
  50. });
  51.  
  52. return originalOpen.apply(this, [method, url, ...args]);
  53. };
  54. }
  55.  
  56. // Function to remove all other HTML elements except for the manga container
  57. function removeOtherElements() {
  58. const bodyChildren = Array.from(document.body.children);
  59. bodyChildren.forEach(child => {
  60. if (!child.id || child.id !== 'manga-container') {
  61. child.remove();
  62. }
  63. });
  64. }
  65.  
  66. // Helper function to create a page container for each image
  67. function createPageContainer(pageUrl) {
  68. const container = document.createElement('div');
  69. container.className = 'manga-page-container';
  70.  
  71. const img = document.createElement('img');
  72. img.src = pageUrl;
  73. img.style.maxWidth = '100%';
  74. img.style.display = 'block';
  75. img.style.margin = '0px auto'; //Note: The 0px dictates the space between images and the auto center them
  76.  
  77. img.onload = function() {
  78. loadedImages++;
  79. updateStats(); // Update the stats when an image is fully loaded
  80. };
  81.  
  82. addClickEventToImage(img);
  83.  
  84. container.appendChild(img);
  85. return container;
  86. }
  87.  
  88. // Function to extract already loaded image URLs from the page
  89. function extractImageUrlsFromPage() {
  90. const images = document.querySelectorAll('.reading-content img.wp-manga-chapter-img');
  91. images.forEach(img => {
  92. const src = img.src.trim();
  93. if (src.startsWith('https://cdn.toonily.com/chapters/manga') && !imageUrls.includes(src)) {
  94. imageUrls.push(src);
  95. }
  96. });
  97. totalImages = imageUrls.length;
  98. }
  99.  
  100. // Helper function to create an exit button
  101. function createExitButton() {
  102. const exitButton = document.createElement('button');
  103. exitButton.textContent = 'Exit';
  104. exitButton.style.backgroundColor = '#e74c3c';
  105. exitButton.style.color = '#fff';
  106. exitButton.style.border = 'none';
  107. exitButton.style.padding = '10px';
  108. exitButton.style.margin = '10px auto';
  109. exitButton.style.display = 'block'; // Center the button
  110. exitButton.style.cursor = 'pointer';
  111. exitButton.style.borderRadius = '5px';
  112.  
  113. exitButton.addEventListener('click', function() {
  114. window.location.reload(); // Reload the page when clicked
  115. });
  116.  
  117. return exitButton;
  118. }
  119.  
  120. // Function to load all manga images into a single strip
  121. function loadMangaImages() {
  122. const mangaContainer = document.createElement('div');
  123. mangaContainer.id = 'manga-container';
  124. document.body.appendChild(mangaContainer);
  125.  
  126. imageUrls.forEach((url, index) => {
  127. const pageContainer = createPageContainer(url);
  128.  
  129. // Add exit button to the first loaded page
  130. if (index === 0) {
  131. const topExitButton = createExitButton();
  132. mangaContainer.appendChild(topExitButton);
  133. }
  134.  
  135. mangaContainer.appendChild(pageContainer);
  136.  
  137. // Add exit button to the last loaded page
  138. if (index === imageUrls.length - 1) {
  139. const bottomExitButton = createExitButton();
  140. mangaContainer.appendChild(bottomExitButton);
  141. }
  142. });
  143.  
  144. removeOtherElements(); // Remove all other page elements after loading
  145. }
  146.  
  147.  
  148. // Function to update the stats window
  149. function updateStats() {
  150. const statsPages = document.querySelector('.ml-stats-pages');
  151. const loadingImages = document.querySelector('.ml-loading-images');
  152. const totalImagesDisplay = document.querySelector('.ml-total-images');
  153. if (statsPages) statsPages.textContent = `${loadedImages}/${totalImages} loaded`;
  154. if (loadingImages) loadingImages.textContent = `${totalImages - loadedImages} images loading`;
  155. if (totalImagesDisplay) totalImagesDisplay.textContent = `${totalImages} images in chapter`;
  156. }
  157.  
  158. // Function to create the stats window
  159. async function createStatsWindow() {
  160. const statsWindow = document.createElement('div');
  161. statsWindow.className = 'ml-stats';
  162. statsWindow.style.position = 'fixed';
  163. statsWindow.style.bottom = '10px';
  164. statsWindow.style.right = '10px';
  165. statsWindow.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
  166. statsWindow.style.padding = '1px';
  167. statsWindow.style.color = 'white';
  168. statsWindow.style.borderRadius = '8px';
  169. statsWindow.style.zIndex = '9999';
  170. statsWindow.style.transition = 'opacity 0.3s';
  171. statsWindow.style.opacity = '0.5';
  172.  
  173. statsWindow.addEventListener('mouseenter', function() {
  174. statsWindow.style.opacity = '1';
  175. });
  176.  
  177. statsWindow.addEventListener('mouseleave', function() {
  178. statsWindow.style.opacity = '0.5';
  179. });
  180.  
  181. const statsWrapper = document.createElement('div');
  182. statsWrapper.style.display = 'flex';
  183. statsWrapper.style.alignItems = 'center'; // Center vertically
  184. const collapseButton = document.createElement('span');
  185. collapseButton.className = 'ml-stats-collapse';
  186. collapseButton.title = 'Hide stats';
  187. collapseButton.textContent = '>>';
  188. collapseButton.style.cursor = 'pointer';
  189. collapseButton.style.marginRight = '10px'; // Space between button and content
  190. collapseButton.addEventListener('click', async function() {
  191. contentContainer.style.display = contentContainer.style.display === 'none' ? 'block' : 'none';
  192. collapseButton.textContent = contentContainer.style.display === 'none' ? '<<' : '>>';
  193. await GM.setValue('statsCollapsed', contentContainer.style.display === 'none');
  194. });
  195. (async () => {
  196. const isCollapsed = await GM.getValue('statsCollapsed', false);
  197. contentContainer.style.display = isCollapsed ? 'none' : 'block';
  198. collapseButton.textContent = isCollapsed ? '<<' : '>>';
  199. })();
  200.  
  201. const contentContainer = document.createElement('div');
  202. contentContainer.className = 'ml-stats-content';
  203.  
  204. const statsText = document.createElement('span');
  205. statsText.className = 'ml-stats-pages';
  206. statsText.textContent = `0/0 loaded`; // Initial stats
  207.  
  208. const infoButton = document.createElement('i');
  209. infoButton.innerHTML = '<i class="fas fa-question-circle"></i>';
  210. infoButton.title = 'See userscript information and help';
  211. infoButton.style.marginLeft = '5px';
  212. infoButton.style.marginRight = '5px'; // Add space to the right
  213. infoButton.addEventListener('click', function() {
  214. alert('This userscript loads manga pages in a single view. Click on an image to reload.');
  215. });
  216.  
  217. const moreStatsButton = document.createElement('i');
  218. moreStatsButton.innerHTML = '<i class="fas fa-chart-pie"></i>';
  219. moreStatsButton.title = 'See detailed page stats';
  220. moreStatsButton.style.marginRight = '5px'; // Add space to the right
  221. moreStatsButton.addEventListener('click', function() {
  222. const statsBox = document.querySelector('.ml-floating-msg');
  223. statsBox.style.display = statsBox.style.display === 'block' ? 'none' : 'block';
  224. });
  225.  
  226. const refreshButton = document.createElement('i');
  227. refreshButton.innerHTML = '<i class="fas fa-sync-alt"></i>';
  228. refreshButton.title = 'Click an image to reload it.';
  229. refreshButton.addEventListener('click', function() {
  230. reloadMode = !reloadMode;
  231. refreshButton.style.color = reloadMode ? 'orange' : '';
  232. console.log(`Reload mode is now ${reloadMode ? 'enabled' : 'disabled'}.`);
  233. });
  234.  
  235. const miniExitButton = document.createElement('button');
  236. miniExitButton.innerHTML = '<i class="fas fa-sign-out-alt"></i>';
  237. miniExitButton.title = 'Exit the Manga Loader';
  238. miniExitButton.style.marginLeft = '10px'; // Space between other buttons
  239. miniExitButton.style.backgroundColor = '#e74c3c'; // Red color for the button
  240. miniExitButton.style.color = '#fff';
  241. miniExitButton.style.border = 'none';
  242. miniExitButton.style.padding = '1px 5px';
  243. miniExitButton.style.borderRadius = '5px';
  244. miniExitButton.style.cursor = 'pointer';
  245.  
  246. miniExitButton.addEventListener('click', function() {
  247. window.location.reload(); // Refresh the page
  248. });
  249.  
  250. contentContainer.appendChild(statsText);
  251. contentContainer.appendChild(infoButton);
  252. contentContainer.appendChild(moreStatsButton);
  253. contentContainer.appendChild(refreshButton);
  254. contentContainer.appendChild(miniExitButton); // Add mini exit button to the content
  255.  
  256. statsWrapper.appendChild(collapseButton);
  257. statsWrapper.appendChild(contentContainer);
  258. statsWindow.appendChild(statsWrapper);
  259.  
  260. const statsBox = document.createElement('pre');
  261. statsBox.className = 'ml-box ml-floating-msg';
  262. statsBox.innerHTML = `<strong>Stats:</strong><br><span class="ml-loading-images">0 images loading</span><br><span class="ml-total-images">0 images in chapter</span><br><span class="ml-loaded-pages">0 pages parsed</span>`;
  263. statsBox.style.display = 'none'; // Initially hidden
  264. statsWindow.appendChild(statsBox);
  265.  
  266. document.body.appendChild(statsWindow);
  267. }
  268.  
  269. // Add the click event to images
  270. function addClickEventToImage(image) {
  271. image.addEventListener('click', function() {
  272. if (reloadMode) {
  273. const imgSrc = image.dataset.src || image.src;
  274. image.src = ''; // Clear the src to trigger reload
  275. setTimeout(() => {
  276. image.src = imgSrc; // Retry loading after clearing
  277. }, 100); // Short delay to ensure proper reload
  278. }
  279. });
  280. }
  281.  
  282. // Create and add the "Load Manga" button
  283. const loadMangaButton = document.createElement('button');
  284. loadMangaButton.textContent = 'Load Manga';
  285. loadMangaButton.style.position = 'fixed';
  286. loadMangaButton.style.bottom = '10px';
  287. loadMangaButton.style.right = '10px';
  288. loadMangaButton.style.zIndex = '9999';
  289. loadMangaButton.style.padding = '10px';
  290. loadMangaButton.style.backgroundColor = '#f39c12';
  291. loadMangaButton.style.border = 'none';
  292. loadMangaButton.style.borderRadius = '5px';
  293. loadMangaButton.style.cursor = 'pointer';
  294.  
  295. // Add hover effect to the button
  296. loadMangaButton.addEventListener('mouseover', function() {
  297. loadMangaButton.style.backgroundColor = '#ff5500';
  298. });
  299.  
  300. loadMangaButton.addEventListener('mouseout', function() {
  301. loadMangaButton.style.backgroundColor = '#f39c12';
  302. });
  303.  
  304. const mangaInfo = document.querySelector("body > div.wrap > div > div > div > div.profile-manga.summary-layout-1 > div > div > div > div.tab-summary");
  305. if (!mangaInfo) {
  306. document.body.appendChild(loadMangaButton);
  307.  
  308. loadMangaButton.addEventListener('click', async function() {
  309. enableLoadAllImages(); // Force the site to load all images
  310. interceptAjaxRequests(); // Hook into AJAX requests to capture image URLs
  311. extractImageUrlsFromPage(); // Initially extract image URLs from the page
  312.  
  313. loadMangaImages(); // Load all collected images
  314. loadMangaButton.remove(); // Remove the button after loading
  315. await createStatsWindow(); // Create the stats window
  316. });
  317. }
  318.  
  319. // Wait for the DOM to finish loading before adding the button
  320. window.addEventListener('DOMContentLoaded', function() {
  321. if (!mangaInfo) {
  322. document.body.appendChild(loadMangaButton);
  323. }
  324. });
  325.  
  326. })();