Nhentai Manga Loader

Loads nhentai manga chapters into one page in a long strip format with image scaling, click events, and a dark mode for reading.

  1. // ==UserScript==
  2. // @name Nhentai Manga Loader
  3. // @namespace http://www.nhentai.net
  4. // @version 6.0.11
  5. // @author longkidkoolstar
  6. // @description Loads nhentai manga chapters into one page in a long strip format with image scaling, click events, and a dark mode for reading.
  7. // @match https://nhentai.net/*
  8. // @require https://code.jquery.com/jquery-3.6.0.min.js
  9. // @icon https://i.imgur.com/S0x03gs.png
  10. // @grant GM.getValue
  11. // @grant GM.setValue
  12. // @grant GM.deleteValue
  13. // @grant GM.listValues
  14. // @license MIT
  15. // @noframes
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. let loadedPages = 0; // Track loaded pages
  22. let totalPages = 0; // Track total pages
  23. let loadingImages = 0; // Track loading images
  24. let totalImages = 0; // Track total images
  25. let freshloadedcache = false;
  26. const mangaId = extractMangaId(window.location.href);
  27.  
  28. // Add this new function to handle jumping to pages
  29. function handleJumpToPage(input) {
  30. const targetPage = parseInt(input.value);
  31. if (isNaN(targetPage) || targetPage < 1 || targetPage > totalPages) {
  32. alert(`Please enter a valid page number between 1 and ${totalPages}`);
  33. return;
  34. }
  35.  
  36. const pageContainers = document.querySelectorAll('.manga-page-container');
  37. const targetContainer = Array.from(pageContainers).find(container => {
  38. const img = container.querySelector('img');
  39. return img && parseInt(img.alt.replace('Page ', '')) === targetPage;
  40. });
  41.  
  42. if (targetContainer) {
  43. // Page is loaded, scroll to it
  44. if (/Mobi/i.test(navigator.userAgent)) {
  45. // Get the offset from the top of the document instead of viewport
  46. const offsetTop = targetContainer.offsetTop; // Add 5px to the top offset
  47. // Scroll to the absolute position
  48. window.scrollTo({
  49. top: offsetTop,
  50. left: 0,
  51. behavior: 'instant' // Use 'instant' for consistent behavior
  52. });
  53. } else {
  54. targetContainer.scrollIntoView({ behavior: 'smooth' });
  55. }
  56. } else {
  57. // Page not loaded, redirect to it
  58. const mangaId = extractMangaId(window.location.href);
  59. loadSpecificPage(targetPage, mangaId);
  60. }
  61.  
  62. // Clear the input after jumping
  63. input.value = '';
  64. }
  65.  
  66.  
  67. (async () => {
  68. const value = JSON.parse(localStorage.getItem('redirected'));
  69. if (value === null) {
  70. localStorage.setItem('redirected', JSON.stringify(false)); // Flag to track if the page has been redirected
  71. }
  72. })();
  73.  
  74. // Helper to create custom style sheets for elements
  75. function addCustomStyles() {
  76. const style = document.createElement('style');
  77. style.innerHTML = `
  78. #manga-container {
  79. max-width: 100vw;
  80. margin: 0 auto;
  81. padding: 0;
  82. }
  83. .manga-page-container {
  84. position: relative;
  85. display: block;
  86. margin: 0;
  87. }
  88. .manga-page-container img {
  89. max-width: 100%;
  90. display: block;
  91. margin: 3px auto;
  92. border-radius: 0;
  93. transition: all 0.3s ease;
  94. box-shadow: none;
  95. }
  96. .ml-counter {
  97. background-color: #222;
  98. color: white;
  99. border-radius: 10px;
  100. width: 40px;
  101. margin-left: auto;
  102. margin-right: auto;
  103. margin-top: -8.8px;
  104. padding-left: 5px;
  105. padding-right: 5px;
  106. border: 1px solid white;
  107. z-index: 100;
  108. position: relative;
  109. font-size: 9px;
  110. font-family: 'Open Sans', sans-serif;
  111. top: 4px;
  112. }
  113. .exit-btn {
  114. background-color: #e74c3c;
  115. color: white;
  116. padding: 5px 10px;
  117. font-size: 14px;
  118. border: none;
  119. border-radius: 8px;
  120. cursor: pointer;
  121. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  122. margin: 10px auto;
  123. display: block;
  124. text-align: center;
  125. }
  126. .exit-btn:hover {
  127. background-color: #c0392b;
  128. }
  129. .exit-btn:active {
  130. background-color: #a93226;
  131. }
  132. .ml-stats {
  133. position: fixed;
  134. bottom: 10px;
  135. right: 10px;
  136. background-color: rgba(0, 0, 0, 0.8);
  137. color: white;
  138. border-radius: 8px;
  139. padding: 3px;
  140. z-index: 1000;
  141. font-family: 'Open Sans', sans-serif;
  142. display: flex;
  143. flex-direction: column;
  144. align-items: flex-start;
  145. }
  146. .ml-stats-content {
  147. display: flex;
  148. align-items: center;
  149. cursor: pointer;
  150. }
  151. .ml-button {
  152. cursor: pointer;
  153. margin-left: 5px;
  154. }
  155. .ml-box {
  156. display: none;
  157. background-color: #333;
  158. color: white;
  159. padding: 10px;
  160. border-radius: 5px;
  161. margin-top: 5px;
  162. width: 200px;
  163. }
  164. `;
  165. document.head.appendChild(style);
  166. }
  167.  
  168. //------------------------------------------------------------------------------**Remove this when transfer over to Nhentai+**------------------------------------------------------------------------------
  169.  
  170. let isPopupVisible = false; // Flag to track if the popup is visible
  171.  
  172. function showPopupForSavedPosition(message, onConfirm, options = {}) {
  173. const existingPopup = document.getElementById('popup');
  174. if (existingPopup) {
  175. document.body.removeChild(existingPopup);
  176. }
  177.  
  178. const popup = document.createElement('div');
  179. popup.id = 'popup';
  180. popup.innerHTML = `
  181. <div class="popup-content" role="alert">
  182. <p>${message}</p>
  183. <button class="confirm-btn">${options.confirmText || 'Yes'}</button>
  184. <button class="cancel-btn">${options.cancelText || 'No'}</button>
  185. </div>
  186. `;
  187. document.body.appendChild(popup);
  188.  
  189. // Set the popup visibility flag
  190. isPopupVisible = true;
  191.  
  192. // Add CSS styling for the popup
  193. const style = document.createElement('style');
  194. style.textContent = `
  195. #popup {
  196. position: fixed;
  197. top: 50%;
  198. left: 50%;
  199. transform: translate(-50%, -50%);
  200. background: rgba(0, 0, 0, 0.9);
  201. color: #fff;
  202. border-radius: 5px;
  203. z-index: 9999;
  204. padding: 15px;
  205. max-width: 300px;
  206. text-align: center;
  207. }
  208. .popup-content {
  209. position: relative;
  210. padding: 10px;
  211. }
  212. .confirm-btn,
  213. .cancel-btn {
  214. margin-top: 10px;
  215. background: none;
  216. border: none;
  217. color: #fff;
  218. font-size: 18px;
  219. cursor: pointer;
  220. transition: color 0.3s, transform 0.3s;
  221. margin: 0 5px; /* Space between buttons */
  222. }
  223. .confirm-btn:hover,
  224. .cancel-btn:hover {
  225. color: #ff0000; /* Change color on hover */
  226. transform: scale(1.1); /* Slightly enlarge on hover */
  227. }
  228. `;
  229. document.head.appendChild(style);
  230.  
  231. // Handle confirmation button click
  232. document.querySelector('.confirm-btn').addEventListener('click', function() {
  233. document.body.removeChild(popup);
  234. document.head.removeChild(style);
  235. isPopupVisible = false; // Reset the flag when popup is closed
  236. if (onConfirm) onConfirm(); // Call the onConfirm callback
  237. });
  238.  
  239. // Handle cancel button click
  240. document.querySelector('.cancel-btn').addEventListener('click', function() {
  241. document.body.removeChild(popup);
  242. document.head.removeChild(style);
  243. isPopupVisible = false; // Reset the flag when popup is closed
  244. });
  245.  
  246. // Auto-close feature based on options
  247. const duration = options.duration || 10000; // Default to 10 seconds if not specified
  248. setTimeout(() => {
  249. if (document.body.contains(popup)) {
  250. document.body.removeChild(popup);
  251. document.head.removeChild(style);
  252. isPopupVisible = false; // Reset the flag when auto-closed
  253. }
  254. }, duration); // Use the specified duration
  255. }
  256.  
  257.  
  258.  
  259. //------------------------------------------------------------------------------**Remove this when transfer over to Nhentai+**------------------------------------------------------------------------------
  260.  
  261. // Function to extract manga ID from URL
  262. function extractMangaId(url) {
  263. const match = url.match(/\/g\/(\d+)/);
  264. return match ? match[1] : null;
  265. }
  266.  
  267. function getCurrentPage(entry) {
  268. const pageElements = document.querySelectorAll('.manga-page-container');
  269. for (let i = 0; i < pageElements.length; i++) {
  270. if (entry.target === pageElements[i]) {
  271. const imgElement = pageElements[i].querySelector('img');
  272. const altText = imgElement.alt;
  273. const pageNumber = parseInt(altText.replace('Page ', ''));
  274. return pageNumber;
  275. }
  276. }
  277. return 1; // Default to page 1 if no current page is found
  278. }
  279.  
  280.  
  281. // Create the "Exit" button
  282. function createExitButton() {
  283. const button = document.createElement('button');
  284. button.textContent = 'Exit';
  285. button.className = 'exit-btn';
  286. return button;
  287. }
  288.  
  289. // Add page counter below the image
  290. function addPageCounter(pageNumber) {
  291. const counter = document.createElement('div');
  292. counter.className = 'ml-counter';
  293. counter.textContent = `${pageNumber}`;
  294. return counter;
  295. }
  296.  
  297. // Update stats display
  298. function updateStats() {
  299. const statsContainer = document.querySelector('.ml-stats-pages');
  300. const statsBox = document.querySelector('.ml-floating-msg');
  301. if (statsBox && !statsBox.querySelector('.jump-controls')) {
  302. statsBox.innerHTML = `<strong>Stats:</strong>
  303. <span class="ml-loading-images">${loadingImages} images loading</span>
  304. <span class="ml-total-images">${totalImages} images in chapter</span>
  305. <span class="ml-loaded-pages">${loadedPages} pages parsed</span>`;
  306.  
  307. if (statsContainer) {
  308. statsContainer.textContent = `${loadedPages}/${totalPages} loaded`;
  309. }
  310. }
  311. }
  312.  
  313. // Declare reloadMode at the top level
  314. let reloadMode = false; // Flag to track reload mode
  315.  
  316. async function createStatsWindow() {
  317. const statsWindow = document.createElement('div');
  318. statsWindow.className = 'ml-stats';
  319.  
  320. // Use a wrapper to keep the button and content aligned
  321. const statsWrapper = document.createElement('div');
  322. statsWrapper.style.display = 'flex';
  323. statsWrapper.style.alignItems = 'center'; // Center vertically
  324.  
  325. const collapseButton = document.createElement('span');
  326. collapseButton.className = 'ml-stats-collapse';
  327. collapseButton.title = 'Hide stats';
  328. collapseButton.textContent = '>>';
  329. collapseButton.style.cursor = 'pointer';
  330. collapseButton.style.marginRight = '10px'; // Space between button and content
  331. collapseButton.addEventListener('click', async function() {
  332. contentContainer.style.display = contentContainer.style.display === 'none' ? 'block' : 'none';
  333. collapseButton.textContent = contentContainer.style.display === 'none' ? '<<' : '>>';
  334.  
  335. // Save the collapse state
  336. await GM.setValue('statsCollapsed', contentContainer.style.display === 'none');
  337. });
  338.  
  339. const contentContainer = document.createElement('div');
  340. contentContainer.className = 'ml-stats-content';
  341.  
  342. const statsText = document.createElement('span');
  343. statsText.className = 'ml-stats-pages';
  344. statsText.textContent = `0/0 loaded`; // Initial stats
  345.  
  346. const infoButton = document.createElement('i');
  347. infoButton.innerHTML = '<i class="fas fa-question-circle"></i>';
  348. infoButton.title = 'See userscript information and help';
  349. infoButton.style.marginLeft = '5px';
  350. infoButton.style.marginRight = '5px'; // Add space to the right
  351. infoButton.addEventListener('click', function() {
  352. alert('This userscript loads manga pages in a single view. It is intended to be used for manga reading and saves your previous scroll position amongst other features.');
  353. });
  354. const moreStatsButton = document.createElement('i');
  355. moreStatsButton.innerHTML = '<i class="fas fa-chart-pie"></i>';
  356. moreStatsButton.title = 'See detailed page stats';
  357. moreStatsButton.style.marginRight = '5px';
  358. moreStatsButton.addEventListener('click', function() {
  359. const statsBox = document.querySelector('.ml-floating-msg');
  360. // If stats box is showing stats content, close it
  361. if (statsBox.style.display === 'block' && statsBox.querySelector('strong').textContent === 'Stats:') {
  362. statsBox.style.display = 'none';
  363. return;
  364. }
  365. // Show stats content
  366. statsBox.style.display = 'block';
  367. statsBox.innerHTML = `<strong>Stats:</strong>
  368. <span class="ml-loading-images">${loadingImages} images loading</span>
  369. <span class="ml-total-images">${totalImages} images in chapter</span>
  370. <span class="ml-loaded-pages">${loadedPages} pages parsed</span>`;
  371. });
  372. // Add new jump page button
  373. const jumpPageButton = document.createElement('i');
  374. jumpPageButton.innerHTML = '<i class="fas fa-search"></i>';
  375. jumpPageButton.title = 'Toggle jump to page';
  376. jumpPageButton.style.marginRight = '5px';
  377. jumpPageButton.addEventListener('click', function() {
  378. const statsBox = document.querySelector('.ml-floating-msg');
  379. // If stats box is showing jump page content, close it
  380. if (statsBox.style.display === 'block' && statsBox.querySelector('strong').textContent === 'Jump to Page') {
  381. statsBox.style.display = 'none';
  382. return;
  383. }
  384. // Show jump page content
  385. statsBox.style.display = 'block';
  386. statsBox.innerHTML = `<strong>Jump to Page</strong>
  387. <div class="jump-controls" style="display: flex; gap: 5px; margin: 5px 0;">
  388. <button class="jump-first">First</button>
  389. <input type="number" class="jump-input" min="1" max="${totalPages}" placeholder="1-${totalPages}">
  390. <button class="jump-last">Last</button>
  391. </div>
  392. <button class="load-saved-position">Load Saved Position</button>
  393. <button class="jump-go">Go</button>`;
  394.  
  395. // Style the input and buttons
  396. const jumpInput = statsBox.querySelector('.jump-input');
  397. jumpInput.style.cssText = `
  398. flex: 2;
  399. width: 50px;
  400. background: #444;
  401. color: #fff;
  402. border: 1px solid #555;
  403. border-radius: 4px;
  404. padding: 2px 4px;
  405. `;
  406.  
  407. // Style all buttons consistently
  408. const buttons = statsBox.querySelectorAll('button');
  409. buttons.forEach(button => {
  410. button.style.cssText = `
  411. background-color: #444;
  412. color: #fff;
  413. border: 1px solid #555;
  414. border-radius: 4px;
  415. padding: 2px 6px;
  416. cursor: pointer;
  417. transition: background-color 0.2s;
  418. width: 100%;
  419. margin-top: 5px;
  420. text-align: left;
  421. `;
  422. });
  423.  
  424. // Special styling for First/Last buttons
  425. const firstLastButtons = statsBox.querySelectorAll('.jump-first, .jump-last');
  426. firstLastButtons.forEach(button => {
  427. button.style.cssText += `
  428. flex: 1;
  429. margin-top: 0;
  430. width: auto;
  431. `;
  432. });
  433.  
  434. // Add event listeners
  435. const loadSavedPositionbtn = statsBox.querySelector('.load-saved-position')
  436. const jumpGo = statsBox.querySelector('.jump-go');
  437. const jumpFirst = statsBox.querySelector('.jump-first');
  438. const jumpLast = statsBox.querySelector('.jump-last');
  439.  
  440. loadSavedPositionbtn.addEventListener('click', () => loadSavedPosition(mangaId));
  441. jumpGo.addEventListener('click', () => handleJumpToPage(jumpInput));
  442. jumpFirst.addEventListener('click', () => handleJumpToPage({ value: '1' }));
  443. jumpLast.addEventListener('click', () => handleJumpToPage({ value: totalPages.toString() }));
  444.  
  445.  
  446. });
  447.  
  448. const refreshButton = document.createElement('i');
  449. refreshButton.innerHTML = '<i class="fas fa-sync-alt"></i>';
  450. refreshButton.title = 'Click an image to reload it.';
  451. refreshButton.addEventListener('click', function() {
  452. reloadMode = !reloadMode;
  453. refreshButton.style.color = reloadMode ? 'orange' : '';
  454. console.log(`Reload mode is now ${reloadMode ? 'enabled' : 'disabled'}.`);
  455. });
  456.  
  457. // Add the mini exit button for refreshing the page
  458. const miniExitButton = document.createElement('button');
  459. miniExitButton.innerHTML = '<i class="fas fa-sign-out-alt"></i>'; // Font Awesome icon for sign out
  460. miniExitButton.title = 'Exit the Manga Loader';
  461. miniExitButton.style.marginLeft = '10px'; // Space between other buttons
  462. miniExitButton.style.backgroundColor = '#e74c3c'; // Red color for the button
  463. miniExitButton.style.color = '#fff';
  464. miniExitButton.style.border = 'none';
  465. miniExitButton.style.padding = '2px 5px';
  466. miniExitButton.style.borderRadius = '5px';
  467. miniExitButton.style.cursor = 'pointer';
  468.  
  469. // Refresh the page when the button is clicked
  470. miniExitButton.addEventListener('click', function() {
  471. window.location.reload(); // Refresh the page
  472. });
  473.  
  474. // Append all elements to the stats content container
  475. contentContainer.appendChild(statsText);
  476. contentContainer.appendChild(infoButton);
  477. contentContainer.appendChild(moreStatsButton);
  478. contentContainer.appendChild(jumpPageButton); // Add the new button
  479. contentContainer.appendChild(refreshButton);
  480. contentContainer.appendChild(miniExitButton);
  481.  
  482. statsWrapper.appendChild(collapseButton);
  483. statsWrapper.appendChild(contentContainer);
  484. statsWindow.appendChild(statsWrapper);
  485.  
  486. const statsBox = document.createElement('pre');
  487. statsBox.className = 'ml-box ml-floating-msg';
  488. statsBox.style.display = 'none'; // Initially hidden
  489.  
  490. // Create the stats content
  491. const statsContent = `<strong>Stats:</strong>
  492. <span class="ml-loading-images">0 images loading</span>
  493. <span class="ml-total-images">0 images in chapter</span>
  494. <span class="ml-loaded-pages">0 pages parsed</span>`;
  495.  
  496. statsBox.innerHTML = statsContent;
  497. statsWindow.appendChild(statsBox);
  498.  
  499. // Check and set initial collapse state
  500. const collapsed = await GM.getValue('statsCollapsed', false);
  501. if (collapsed) {
  502. contentContainer.style.display = 'none';
  503. collapseButton.textContent = '<<'; // Change to indicate expanded state
  504. }
  505.  
  506. // Add hover effect
  507. statsWindow.style.transition = 'opacity 0.3s';
  508. statsWindow.style.opacity = '0.6'; // Dimmed by default
  509.  
  510. statsWindow.addEventListener('mouseenter', function() {
  511. statsWindow.style.opacity = '1'; // Fully visible on hover
  512. });
  513.  
  514. statsWindow.addEventListener('mouseleave', function() {
  515. statsWindow.style.opacity = '0.6'; // Dim again on mouse leave
  516. });
  517.  
  518. document.body.appendChild(statsWindow);
  519. }
  520.  
  521.  
  522.  
  523. // Add the click event to images
  524. function addClickEventToImage(image) {
  525. image.addEventListener('click', function() {
  526. if (reloadMode) {
  527. const imgSrc = image.dataset.src || image.src;
  528. image.src = ''; // Clear the src to trigger reload
  529. setTimeout(() => {
  530. image.src = imgSrc; // Retry loading after clearing
  531. }, 100); // Short delay to ensure proper reload
  532. }
  533. });
  534. }
  535.  
  536.  
  537.  
  538. // Function to hide specified elements
  539. function hideElements() {
  540. const elementsToHide = ['#image-container', '#content', 'nav'];
  541. elementsToHide.forEach(selector => {
  542. const element = document.querySelector(selector);
  543. if (element) {
  544. element.style.display = 'none';
  545. }
  546. });
  547. }
  548.  
  549. // Add this at the top level to track image loading status
  550. const imageStatus = []; // Array to track the status of each image
  551.  
  552. // Add an event listener to detect when the user scrolls
  553. window.addEventListener('scroll', logCurrentPage);
  554.  
  555. // Variable to store the previous page
  556. let previousPage = 0;
  557.  
  558. // Function to log the current page
  559. function logCurrentPage() {
  560. // Check if the URL matches the desired pattern
  561. if (!window.location.href.match(/^https:\/\/nhentai\.net\/g\//)) {
  562. return; // Exit if the URL is not correct
  563. }
  564.  
  565. // Check if the download button exists
  566. if (document.querySelector("#download")) {
  567. return; // Exit if the download button exists
  568. }
  569.  
  570. const currentPage = getCurrentVisiblePage();
  571. const totalPages = document.querySelectorAll('.manga-page-container').length;
  572.  
  573. // Check if the Load Manga button exists
  574. const loadMangaButton = document.querySelector('.load-manga-btn');
  575. if (loadMangaButton) {
  576. return; // Exit if the Load Manga button exists
  577. }
  578.  
  579. if ((currentPage === totalPages - 1 || currentPage === totalPages) && (!isPopupVisible || freshloadedcache)) {
  580. //console.log(`Current page: ${currentPage}`);
  581. previousPage = currentPage;
  582. if (currentPage >= totalPages - 1) deleteMangaFromStorage();
  583. }
  584. }
  585.  
  586. function getCurrentVisiblePage() {
  587. const pageContainers = document.querySelectorAll('.manga-page-container');
  588. let visiblePage = 0;
  589. const totalPages = pageContainers.length;
  590. // No pages found
  591. if (totalPages === 0) {
  592. // console.warn('No page containers found.');
  593. return visiblePage;
  594. }
  595. // Determine if device is mobile or desktop based on screen width
  596. const isMobile = window.innerWidth <= 768; // Common breakpoint for mobile
  597. // Use different thresholds based on device type
  598. const visibilityThreshold = isMobile ? 70 : 25; // Lower threshold for desktop
  599. pageContainers.forEach((container, index) => {
  600. const img = container.querySelector('img');
  601. if (img && img.alt) {
  602. const pageNumber = parseInt(img.alt.replace('Page ', ''), 10);
  603. const rect = img.getBoundingClientRect();
  604. const pageHeight = rect.bottom - rect.top;
  605. const visibleHeight = Math.min(window.innerHeight, rect.bottom) - Math.max(0, rect.top);
  606. const visiblePercentage = (visibleHeight / pageHeight) * 100;
  607. if (visiblePercentage >= visibilityThreshold) {
  608. visiblePage = pageNumber;
  609. }
  610. // Keep the last page logic
  611. if (index + 1 === totalPages && visiblePercentage >= 10) {
  612. visiblePage = totalPages;
  613. }
  614. }
  615. });
  616. // Fallback logic remains the same
  617. if (visiblePage === 0) {
  618. const currentPageMatch = window.location.pathname.match(/\/g\/\d+\/(\d+)/);
  619. if (currentPageMatch) {
  620. visiblePage = parseInt(currentPageMatch[1], 10);
  621. }
  622. }
  623. //console.log("Current visible page determined:", visiblePage);
  624. return visiblePage;
  625. }
  626.  
  627.  
  628. // Function to delete manga from storage
  629. function deleteMangaFromStorage() {
  630. const mangaId = window.location.pathname.match(/\/g\/(\d+)/)[1];
  631. GM.deleteValue(mangaId); // Delete the manga entry
  632.  
  633. // Check if metadata exists before attempting to delete it
  634. GM.getValue(`metadata_${mangaId}`).then(metadata => {
  635. if (metadata) {
  636. GM.deleteValue(`metadata_${mangaId}`); // Delete the associated metadata
  637. console.log(`Metadata for manga ${mangaId} deleted from storage`);
  638. } else {
  639. console.log(`No metadata found for manga ${mangaId}, skipping deletion`);
  640. }
  641. });
  642.  
  643. console.log(`Manga ${mangaId} deleted from storage`);
  644. }
  645.  
  646. // Replace the addScrollListener function with the following code
  647. let previousPagex = 0; // Initialize previousPage at the top level
  648.  
  649. window.addEventListener('scroll', async () => {
  650. const currentPage = getCurrentVisiblePage();
  651. //console.log("current page:", currentPage, "last page", previousPagex);
  652. // Only save the current page if the popup is not visible and the current page is greater than the previous page
  653. if (!isPopupVisible || freshloadedcache) {
  654. if (currentPage > previousPagex) {
  655. console.log(`Current page: ${currentPage}, Previous page: ${previousPagex}`);
  656. await saveCurrentPosition(mangaId, currentPage);
  657. previousPagex = currentPage; // Update previousPage to the current page
  658. }
  659. }
  660. });
  661. // Log the state of freshloadedcache every second
  662. setInterval(() => {
  663. // console.log(`Fresh loaded cache state: ${freshloadedcache}`);
  664. }, 1000);
  665.  
  666.  
  667. // Load all manga images with page separators and scaling
  668. function loadMangaImages(mangaId) {
  669. hideElements();
  670. createStatsWindow(); // Create the stats window
  671.  
  672. const mangaContainer = document.createElement('div');
  673. mangaContainer.id = 'manga-container';
  674. document.body.appendChild(mangaContainer);
  675.  
  676.  
  677. const exitButtonTop = createExitButton();
  678. mangaContainer.appendChild(exitButtonTop);
  679.  
  680. totalPages = parseInt(document.querySelector('.num-pages').textContent.trim());
  681. totalImages = totalPages; // Update total images for stats
  682. const initialPage = parseInt(window.location.href.match(/\/g\/\d+\/(\d+)/)[1]);
  683. let currentPage = initialPage;
  684.  
  685. // Queue for tracking loading images
  686. const loadingQueue = [];
  687. const maxConcurrentLoads = /Mobi/.test(navigator.userAgent) ? 10 : 40; // Maximum number of concurrent image loads
  688.  
  689. // Helper to create the page container with images
  690. function createPageContainer(pageNumber, imgSrc) {
  691. const container = document.createElement('div');
  692. container.className = 'manga-page-container';
  693.  
  694. // Create the actual image element
  695. const img = document.createElement('img');
  696. img.src = ''; // Start with empty src to avoid loading it immediately
  697. img.dataset.src = imgSrc; // Store the actual src in data attribute
  698. img.alt = `Page ${pageNumber}`;
  699.  
  700. // Add page counter below the image
  701. const pageCounter = addPageCounter(pageNumber);
  702.  
  703. // Append the image and page counter
  704. container.appendChild(img);
  705. container.appendChild(pageCounter); // <-- Page number is shown here
  706.  
  707. // Add exit button to the bottom of the last loaded page
  708. if (pageNumber === totalPages) {
  709. const exitButton = createExitButton();
  710. container.appendChild(exitButton);
  711. exitButton.addEventListener('click', () => {
  712. window.location.reload();
  713. })
  714. }
  715.  
  716. // Track the image status
  717. imageStatus[pageNumber] = { src: imgSrc, loaded: false, attempts: 0 };
  718.  
  719. // Error handling and event listeners
  720. addErrorHandlingToImage(img, imgSrc, pageNumber);
  721. addClickEventToImage(img);
  722. mangaContainer.appendChild(container);
  723.  
  724. loadedPages++; // Increment loaded pages count
  725. updateStats(); // Update stats display
  726.  
  727. observePageContainer(container); // Observe for lazy loading
  728.  
  729. // Save scroll position as soon as page container is created
  730. const mangaId = extractMangaId(window.location.href);
  731. const currentPage = getCurrentVisiblePage(); // Get the current visible page number
  732. if (!isPopupVisible || freshloadedcache) {
  733. // console.log("load again");
  734. // saveCurrentPosition(mangaId, currentPage);
  735. }
  736.  
  737. // Start loading the actual image
  738. img.src = imgSrc; // Set the src to load the image
  739.  
  740. // Mark as loaded on load
  741. img.onload = () => {
  742. imageStatus[pageNumber].loaded = true; // Mark as loaded
  743. loadingImages--; // Decrement loading images count
  744. updateStats(); // Update loading images count
  745. };
  746.  
  747. return container;
  748. }
  749.  
  750.  
  751.  
  752.  
  753.  
  754.  
  755.  
  756. // Add a delay function
  757. function delay(ms) {
  758. return new Promise(resolve => setTimeout(resolve, ms));
  759. }
  760.  
  761. // Track if the app is online or offline
  762. let isOnline = navigator.onLine;
  763.  
  764. // Add event listeners to detect connection state changes
  765. window.addEventListener('offline', () => {
  766. console.warn('You are offline. Pausing image loading.');
  767. isOnline = false;
  768. });
  769.  
  770. window.addEventListener('online', () => {
  771. console.log('Back online. Resuming image loading.');
  772. isOnline = true;
  773. if (loadingQueue.length > 0) {
  774. processQueue(); // Resume processing the queue
  775. } else {
  776. // If queue is empty, manually trigger the next page load
  777. loadNextBatchOfImages(); // Load the next set of images if queue is empty
  778. }
  779. });
  780.  
  781. // Load a single page with error handling, retry logic, and caching
  782. async function loadPage(pageNumber, pageUrl, retryCount = 0) {
  783. if (loadingImages >= maxConcurrentLoads || !isOnline) {
  784. return; // Exit if we're at max concurrent loads or offline
  785. }
  786.  
  787. loadingImages++;
  788. updateStats(); // Update loading images count
  789.  
  790. const mangaId = extractMangaId(pageUrl);
  791. if (!mangaId) {
  792. console.error(`Could not extract manga ID from URL: ${pageUrl}`);
  793. loadingImages--;
  794. updateStats();
  795. handleFailedImage(pageNumber);
  796. return;
  797. }
  798.  
  799. // Check cache first
  800. const cachedImage = getImageFromCache(pageNumber, mangaId);
  801. if (cachedImage && cachedImage.mangaId === mangaId) {
  802. console.log(`Loading page ${pageNumber} from cache for manga ${mangaId}`);
  803. const pageContainer = createPageContainer(pageNumber, cachedImage.imgSrc);
  804. imageStatus[pageNumber].loaded = true; // Mark as loaded
  805. // Ensure position is saved for cached pages
  806. const currentPage = pageNumber;
  807. // console.log("load");
  808. // saveCurrentPosition(mangaId, currentPage); // Save the position for cached pages
  809.  
  810. loadingImages--;
  811. updateStats(); // Update loading images count
  812.  
  813. // Pre-fetch the next page if it's not the last page
  814. if (pageNumber < totalPages && cachedImage.nextLink) {
  815. loadingQueue.push({ pageNumber: pageNumber + 1, pageUrl: cachedImage.nextLink });
  816. processQueue(); // Check the queue
  817. }
  818. return;
  819. }
  820.  
  821. try {
  822. const response = await fetch(pageUrl);
  823.  
  824. if (response.status === 429) {
  825. if (retryCount < maxRetries) {
  826. console.warn(`Rate limit exceeded for page ${pageNumber}. Retrying in ${retryDelay} ms...`);
  827. await delay(retryDelay); // Wait before retrying
  828. loadPage(pageNumber, pageUrl, retryCount + 1); // Retry loading the same page
  829. return;
  830. } else {
  831. console.error(`Failed to load page ${pageNumber} after ${maxRetries} attempts.`);
  832. loadingImages--;
  833. updateStats(); // Update loading images count
  834. handleFailedImage(pageNumber); // Handle failed image loading
  835. return;
  836. }
  837. }
  838.  
  839. const html = await response.text();
  840. const parser = new DOMParser();
  841. const doc = parser.parseFromString(html, 'text/html');
  842. const imgElement = doc.querySelector('#image-container > a > img');
  843. const nextLink = doc.querySelector('#image-container > a').href;
  844. const imgSrc = imgElement.getAttribute('data-src') || imgElement.src;
  845.  
  846. // Save to cache
  847. saveImageToCache(pageNumber, imgSrc, nextLink, mangaId);
  848.  
  849. const pageContainer = createPageContainer(pageNumber, imgSrc);
  850. imageStatus[pageNumber].loaded = true; // Mark as loaded
  851.  
  852. loadingImages--;
  853. updateStats(); // Update loading images count
  854.  
  855. // Pre-fetch the next page once the current one loads
  856. if (pageNumber < totalPages && nextLink) {
  857. loadingQueue.push({ pageNumber: pageNumber + 1, pageUrl: nextLink });
  858. processQueue(); // Check the queue
  859. }
  860. } catch (err) {
  861. loadingImages--;
  862. console.error(err);
  863. updateStats(); // Update loading images count
  864. handleFailedImage(pageNumber); // Handle failed image loading
  865. }
  866. }
  867.  
  868. // In your processing queue, ensure a delay ONLY after 429 status
  869. async function processQueue() {
  870. while (loadingQueue.length > 0 && loadingImages < maxConcurrentLoads && isOnline) {
  871. const { pageNumber, pageUrl } = loadingQueue.shift(); // Get the next page to load
  872. loadPage(pageNumber, pageUrl); // Load it
  873. }
  874. }
  875.  
  876. // Manually trigger the next batch of images if needed
  877. function loadNextBatchOfImages() {
  878. if (loadingQueue.length === 0 && isOnline) {
  879. const nextPageNumber = getNextPageNumber(); // Logic to get the next page number
  880. const nextPageUrl = getNextPageUrl(nextPageNumber); // Logic to get the next page URL
  881.  
  882. if (nextPageUrl) {
  883. loadingQueue.push({ pageNumber: nextPageNumber, pageUrl: nextPageUrl });
  884. processQueue(); // Resume loading
  885. }
  886. }
  887. }
  888.  
  889. // Configuration for retry logic
  890. const maxRetries = 5; // Maximum number of retries for rate limit
  891. const retryDelay = 5000; // Delay in milliseconds before retrying only on 429 status
  892.  
  893.  
  894.  
  895.  
  896.  
  897. // Handle failed image loading attempts
  898. function handleFailedImage(pageNumber) {
  899. if (imageStatus[pageNumber]) {
  900. imageStatus[pageNumber].attempts++;
  901. if (imageStatus[pageNumber].attempts <= 3) { // Retry up to 3 times
  902. console.warn(`Retrying to load image for page ${pageNumber}...`);
  903. loadPage(pageNumber, document.querySelector(`#image-container > a`).href); // Reattempt loading the same page
  904. } else {
  905. console.error(`Failed to load image for page ${pageNumber} after 3 attempts.`);
  906. }
  907. }
  908. }
  909.  
  910.  
  911.  
  912. const firstImageElement = document.querySelector('#image-container > a > img');
  913. const firstImgSrc = firstImageElement.getAttribute('data-src') || firstImageElement.src;
  914. createPageContainer(currentPage, firstImgSrc);
  915.  
  916. const firstImageLink = document.querySelector('#image-container > a').href;
  917. loadingQueue.push({ pageNumber: currentPage + 1, pageUrl: firstImageLink }); // Add to queue
  918. processQueue(); // Start processing the queue
  919.  
  920. // Observe all image containers for lazy loading
  921. observeAndPreloadImages(); // <-- Add this here to track and lazy-load images
  922.  
  923. exitButtonTop.addEventListener('click', function() {
  924. window.location.reload();
  925. });
  926. }
  927.  
  928. // Pre-load next few images while user scrolls
  929. function observeAndPreloadImages() {
  930. const observer = new IntersectionObserver((entries) => {
  931. entries.forEach((entry) => {
  932. if (entry.isIntersecting) {
  933. const imgElement = entry.target.querySelector('img');
  934. if (imgElement && imgElement.dataset.src) {
  935. imgElement.src = imgElement.dataset.src; // Load the image
  936. observer.unobserve(entry.target); // Stop observing after loading
  937. // Save the current position
  938. const mangaId = extractMangaId(window.location.href);
  939. const currentPage = getCurrentVisiblePage(entry); // Get the current page number
  940. if (!isPopupVisible || freshloadedcache) {
  941. //console.log("preload");
  942. // saveCurrentPosition(mangaId, currentPage);
  943. }
  944. }
  945. }
  946. });
  947. }, {
  948. rootMargin: '300px 0px', // Load images 300px before they appear
  949. threshold: 0.1
  950. });
  951. // Observe each image container
  952. const imageContainers = document.querySelectorAll('.manga-page-container');
  953. imageContainers.forEach((container) => observer.observe(container));
  954. }
  955.  
  956. // Function to get image data from local storage
  957. function getImageFromCache(pageNumber, mangaId) {
  958. // console.log("freshloadedcache", freshloadedcache);
  959. freshloadedcache = true;
  960. setInterval(() => {
  961. //console.log("freshloadedcache", freshloadedcache);
  962. freshloadedcache = false;
  963. }, 3000)
  964. const cacheKey = `imagePage_${mangaId}_${pageNumber}`;
  965. const cachedData = localStorage.getItem(cacheKey);
  966. if (cachedData) {
  967. return JSON.parse(cachedData);
  968. }
  969. return null;
  970. }
  971.  
  972. // Function to save image data to local storage
  973. function saveImageToCache(pageNumber, imgSrc, nextLink, mangaId) {
  974. const cacheKey = `imagePage_${mangaId}_${pageNumber}`;
  975. const cacheData = { imgSrc, nextLink, timestamp: Date.now(), mangaId };
  976. localStorage.setItem(cacheKey, JSON.stringify(cacheData));
  977. }
  978.  
  979.  
  980. function addErrorHandlingToImage(image, imgSrc, pageNumber) {
  981. const subdomains = ['i1', 'i2', 'i3', 'i4', 'i5', 'i7']; // Add the alternative subdomains here
  982. let currentSubdomainIndex = 0;
  983.  
  984. function updateImageSource(newSrc) {
  985. image.src = newSrc;
  986. image.dataset.src = newSrc; // Update data-src attribute
  987. updateImageCache(newSrc);
  988. }
  989.  
  990. function updateImageCache(newSrc) {
  991. const mangaId = extractMangaId(window.location.href);
  992. const cachedData = getImageFromCache(pageNumber, mangaId);
  993. if (cachedData) {
  994. cachedData.imgSrc = newSrc;
  995. saveImageToCache(pageNumber, newSrc, cachedData.nextLink, mangaId);
  996. console.log(`Updated cache for page ${pageNumber} with new URL: ${newSrc}`);
  997. }
  998. }
  999.  
  1000. image.onerror = function() {
  1001. console.warn(`Failed to load image: ${imgSrc} on page ${pageNumber}. Retrying...`);
  1002. if (!imageStatus[pageNumber].retryCount) {
  1003. imageStatus[pageNumber].retryCount = 0;
  1004. }
  1005.  
  1006. if (imageStatus[pageNumber].retryCount < subdomains.length) {
  1007. imageStatus[pageNumber].retryCount++;
  1008. const newSubdomain = subdomains[currentSubdomainIndex];
  1009. const newImgSrc = imgSrc.replace(/i\d/, newSubdomain);
  1010.  
  1011. currentSubdomainIndex = (currentSubdomainIndex + 1) % subdomains.length;
  1012. console.log(`Retrying with new subdomain: ${newSubdomain} for page ${pageNumber}`);
  1013. setTimeout(() => {
  1014. updateImageSource(newImgSrc);
  1015. // Update the local storage cache for this page
  1016. const mangaId = extractMangaId(window.location.href);
  1017. const cachedData = getImageFromCache(pageNumber, mangaId);
  1018. if (cachedData) {
  1019. saveImageToCache(pageNumber, newImgSrc, cachedData.nextLink, mangaId);
  1020. console.log(`Updated local storage cache for page ${pageNumber} with new URL: ${newImgSrc}`);
  1021. }
  1022. }, 1000);
  1023. } else {
  1024. console.error(`Failed to load image on page ${pageNumber} after multiple attempts.`);
  1025. image.alt = `Failed to load page ${pageNumber}`;
  1026. }
  1027. };
  1028.  
  1029. // Update cache even if image loads successfully from cache
  1030. image.onload = function() {
  1031. updateImageCache(image.src);
  1032. };
  1033. }
  1034.  
  1035.  
  1036. // Create an IntersectionObserver to prioritize loading images that are in or near the viewport
  1037. // Create an IntersectionObserver to prioritize loading images that are in or near the viewport
  1038. const observer = new IntersectionObserver((entries) => {
  1039. entries.forEach(entry => {
  1040. if (entry.isIntersecting) {
  1041. const imgElement = entry.target.querySelector('img');
  1042. if (imgElement && imgElement.dataset.src) {
  1043. imgElement.src = imgElement.dataset.src; // Load the image
  1044. observer.unobserve(entry.target); // Stop observing after loading
  1045. // Save the current scroll position as soon as the image starts loading
  1046. const mangaId = extractMangaId(window.location.href);
  1047. const currentPage = getCurrentVisiblePage(); // Get the current visible page number
  1048. if (!isPopupVisible || freshloadedcache) {
  1049. //console.log("intesect");
  1050. // saveCurrentPosition(mangaId, currentPage);
  1051. }
  1052. }
  1053. }
  1054. });
  1055. }, {
  1056. rootMargin: '200px 0px', // Adjust for preloading images slightly outside the viewport
  1057. threshold: 0.1 // Trigger loading when 10% of the image is in view
  1058. });
  1059.  
  1060. function observePageContainer(container) {
  1061. observer.observe(container); // Observe each page container for lazy loading
  1062. }
  1063.  
  1064. addCustomStyles();
  1065.  
  1066.  
  1067. // Compress data into a string format
  1068. function compressData(data) {
  1069. return JSON.stringify(data);
  1070. }
  1071.  
  1072. // Decompress data from string format
  1073. function decompressData(data) {
  1074. return JSON.parse(data);
  1075. }
  1076.  
  1077. async function storeData(mangaId, pageNum) {
  1078. const existingData = await retrieveData(mangaId);
  1079. const existingPageNum = existingData ? existingData.pageNum : 0;
  1080.  
  1081. if (pageNum > existingPageNum) {
  1082. const currentTime = Date.now();
  1083. const data = { pageNum, lastAccessed: currentTime };
  1084. const compressedData = compressData(data);
  1085. await GM.setValue(mangaId, compressedData);
  1086.  
  1087. // Manage storage size if it exceeds the limit
  1088. await manageStorage();
  1089. }
  1090. }
  1091.  
  1092. // Retrieve data from Tampermonkey storage
  1093. async function retrieveData(mangaId) {
  1094. const compressedData = await GM.getValue(mangaId, null);
  1095. if (compressedData) {
  1096. return decompressData(compressedData);
  1097. }
  1098. return null;
  1099. }
  1100.  
  1101. // Delete the least recently accessed data if the limit is reached
  1102. async function manageStorage() {
  1103. const MAX_ENTRIES = 52; // Limit to store 50 recent hentai
  1104. const keys = await GM.listValues();
  1105. if (keys.length > MAX_ENTRIES) {
  1106. const entries = [];
  1107. for (let key of keys) {
  1108. const value = await GM.getValue(key);
  1109. const data = decompressData(value);
  1110. entries.push({ key, lastAccessed: data.lastAccessed });
  1111. }
  1112.  
  1113. // Sort by last accessed time, oldest first
  1114. entries.sort((a, b) => a.lastAccessed - b.lastAccessed);
  1115.  
  1116. // Remove the oldest entries until we're under the limit
  1117. const excess = entries.length - MAX_ENTRIES;
  1118. for (let i = 0; i < excess; i++) {
  1119. await GM.deleteValue(entries[i].key);
  1120. }
  1121. }
  1122. }
  1123.  
  1124. let isRestoringPosition = false; // Flag to prevent overwriting saved position
  1125.  
  1126.  
  1127.  
  1128. function getCurrentPageFromURL() {
  1129. const match = window.location.pathname.match(/\/g\/\d+\/(\d+)\//);
  1130. return match ? parseInt(match[1], 10) : 1; // Default to 1 if not found
  1131. }
  1132.  
  1133.  
  1134. async function loadSavedPosition(mangaId) {
  1135. console.log(`Trying to load saved position for: ${mangaId}`);
  1136.  
  1137. const savedData = await retrieveData(mangaId);
  1138. console.log(`Saved data retrieved:`, savedData); // Log the retrieved data
  1139.  
  1140. const savedPage = savedData.pageNum;
  1141. if (savedPage && savedPage === totalPages) {
  1142. await GM.deleteValue(mangaId);
  1143. console.log(`Saved position deleted for ${mangaId} since it's equal to total pages.`);
  1144. return;
  1145. }
  1146.  
  1147. if (savedData) {
  1148. const savedPage = savedData.pageNum;
  1149. console.log(`Saved page is: ${savedPage}`); // Log the saved page number
  1150.  
  1151. const currentPage = getCurrentPageFromURL(); // Get current page from URL
  1152. console.log(`Current page is: ${currentPage}`); // Log the current page number
  1153.  
  1154. // Only proceed if the saved page is different from the current one
  1155. if (savedPage && savedPage !== currentPage) {
  1156. console.log(`Restoring to saved page: ${savedPage}`);
  1157. isRestoringPosition = true; // Set the flag before restoring the position
  1158.  
  1159. const pageContainers = document.querySelectorAll('.manga-page-container');
  1160. console.log(`Total page containers loaded: ${pageContainers.length}`); // Log how many pages are loaded
  1161.  
  1162. if (pageContainers.length > 0) {
  1163. // Directly scroll to the saved page
  1164. scrollToSavedPage(pageContainers, savedPage);
  1165. } else {
  1166. console.log(`Waiting for pages to load...`);
  1167. waitForPageContainers(savedPage); // Use a MutationObserver to wait for containers
  1168. }
  1169. } else {
  1170. console.log(`Not restoring saved position for ${mangaId}. Current page is the same as saved page.`);
  1171. }
  1172. } else {
  1173. console.log(`No saved position found for ${mangaId}.`);
  1174. }
  1175.  
  1176. isRestoringPosition = false; // Reset the flag after restoring the position
  1177. }
  1178.  
  1179. function waitForPageContainers(savedPageWithOffset) {
  1180. const observer = new MutationObserver((mutations, obs) => {
  1181. const pageContainers = document.querySelectorAll('.manga-page-container');
  1182. if (pageContainers.length >= savedPageWithOffset) {
  1183. console.log(`Page containers are now loaded: ${pageContainers.length}`);
  1184. obs.disconnect(); // Stop observing once the pages are loaded
  1185. scrollToSavedPage(pageContainers, savedPageWithOffset); // Scroll to the saved page
  1186. }
  1187. });
  1188.  
  1189. // Observe changes in the DOM (specifically looking for added nodes)
  1190. observer.observe(document.body, {
  1191. childList: true,
  1192. subtree: true
  1193. });
  1194. }
  1195.  
  1196. // Queue for specific page loading
  1197. const specificPageQueue = [];
  1198.  
  1199.  
  1200.  
  1201. // Function to load a specific page by redirecting to its URL
  1202. async function loadSpecificPage(pageNumber) {
  1203. const mangaId = extractMangaId(window.location.href); // Extract manga ID from current URL
  1204. const pageUrl = `https://nhentai.net/g/${mangaId}/${pageNumber}/`; // Construct the URL for the specific page
  1205.  
  1206. console.log(`Redirecting to page ${pageNumber} at URL: ${pageUrl}`);
  1207. localStorage.setItem('redirected', 'true'); // Save the redirected state in local storage
  1208. console.log(`Set redirected flag to true in storage.`); // Log confirmation of setting the flag
  1209.  
  1210. window.location.href = pageUrl; // Redirect to the specific page URL
  1211. }
  1212.  
  1213. // Function to check if the page is redirected and load manga images
  1214. async function checkRedirected() {
  1215. const wasRedirected = JSON.parse(localStorage.getItem('redirected') || 'false'); // Retrieve the redirected state
  1216.  
  1217. if (wasRedirected) {
  1218. const mangaId = extractMangaId(window.location.href);
  1219. console.log(`Loading manga images for manga ID: ${mangaId}`); // Log the manga ID
  1220. loadMangaButton.remove(); // Remove the load manga button since we already did it
  1221. loadMangaImages(mangaId); // Call loadMangaImages after redirection
  1222. console.log(`Reset redirected flag to false in storage.`); // Log confirmation of resetting the flag
  1223. localStorage.setItem('redirected', JSON.stringify(false)); // Reset the flag in storage
  1224. }
  1225. }
  1226. // Call the function every second
  1227. setInterval(checkRedirected, 1000);
  1228.  
  1229.  
  1230.  
  1231.  
  1232.  
  1233. async function scrollToSavedPage(pageContainers, savedPage, savedImgSrc) {
  1234. const currentPage = getCurrentPageFromURL(); // Get current page number from URL
  1235. const savedPageIndex = savedPage - currentPage; // Calculate the effective saved page index
  1236.  
  1237. console.log(`Current page: ${currentPage}, Adjusted index for saved page: ${savedPageIndex}`);
  1238.  
  1239. // Check if the adjusted index is out of bounds
  1240. if (savedPageIndex < 0 || savedPageIndex >= pageContainers.length) {
  1241. console.warn(`Adjusted saved page index ${savedPageIndex} is out of bounds.`);
  1242. console.log(`Page ${savedPage} is not loaded yet. Redirecting to its URL.`);
  1243. loadSpecificPage(savedPage); // Redirect to the specific page
  1244. return; // Exit early
  1245. }
  1246.  
  1247. const savedPageElement = pageContainers[savedPageIndex]; // Get the container for the saved page
  1248. const img = savedPageElement.querySelector('img');
  1249.  
  1250.  
  1251. // If the image is loaded
  1252. if (img && img.complete) {
  1253. console.log(`Image for page ${savedPage} loaded. Moving to it.`);
  1254. if (/Mobi/i.test(navigator.userAgent)) {
  1255. const rect = savedPageElement.getBoundingClientRect();
  1256. window.scrollTo(rect.left, rect.top); // teleport on mobile
  1257. } else {
  1258. savedPageElement.scrollIntoView({ behavior: 'smooth' }); // scroll on desktop
  1259. }
  1260. } else {
  1261. console.log(`Image for page ${savedPage} not loaded yet. Redirecting to its URL.`);
  1262. loadSpecificPage(savedPage); // Redirect to the specific page
  1263. }
  1264. }
  1265.  
  1266. //----------------------------------------------Make the second option later When in Main Script----------------------------------------------
  1267.  
  1268. // If the image is loaded
  1269. // if (img && img.complete) {
  1270. // console.log(`Image for page ${savedPage} loaded. Scrolling to it.`);
  1271. // savedPageElement.scrollIntoView({ behavior: 'smooth' });
  1272. // }
  1273.  
  1274.  
  1275. // If the image is loaded
  1276. // if (img && img.complete) {
  1277. // console.log(`Image for page ${savedPage} loaded. Scrolling to it.`);
  1278. // savedPageElement.scrollIntoView({ behavior: 'smooth' });
  1279. // }
  1280.  
  1281. //----------------------------------------------Make the second option later When in Main Script----------------------------------------------
  1282.  
  1283.  
  1284.  
  1285. function getNextPageUrl(pageNumber) {
  1286. console.log(`Searching for URL for page ${pageNumber}`);
  1287. const pageLink = document.querySelector(`#image-container a[href*='/g/'][href*='/${pageNumber}/']`);
  1288. console.log(`Found page link: ${pageLink}`);
  1289. if (pageLink) {
  1290. return pageLink.href;
  1291. } else {
  1292. console.log(`No URL found for page ${pageNumber}`);
  1293. return null;
  1294. }
  1295. }
  1296.  
  1297.  
  1298.  
  1299.  
  1300. // Save the current position without checking for visibility// Save the current position based on the current page
  1301. async function saveCurrentPosition(mangaId) {
  1302. const totalPages = document.querySelectorAll('.manga-page-container').length;
  1303. const currentPage = getCurrentVisiblePage(); // Get the current page number from the URL
  1304.  
  1305. // Log the total pages and current page for debugging
  1306. console.log(`Total pages loaded: ${totalPages}, trying to save position for page: ${currentPage}`);
  1307.  
  1308. // Always save the position
  1309. if (!isRestoringPosition) { // Only save if we are not restoring
  1310. await storeData(mangaId, currentPage);
  1311. console.log(`Position saved: Manga ID: ${mangaId}, Page: ${currentPage}`);
  1312. } else {
  1313. console.log(`Not saving position for Manga ID: ${mangaId} as we are restoring.`);
  1314. }
  1315. }
  1316.  
  1317.  
  1318. // Periodically clean up storage
  1319. manageStorage();
  1320.  
  1321.  
  1322. window.loadMangaButton = document.createElement('button');
  1323. loadMangaButton.textContent = 'Load Manga';
  1324. loadMangaButton.className = 'load-manga-btn';
  1325. loadMangaButton.style.position = 'fixed';
  1326. loadMangaButton.style.bottom = '0';
  1327. loadMangaButton.style.right = '0';
  1328. loadMangaButton.style.padding = '5px';
  1329. loadMangaButton.style.margin = '0 10px 10px 0';
  1330. loadMangaButton.style.zIndex = '9999999999';
  1331.  
  1332. if (window.location.href.startsWith("https://nhentai.net/g/")) {
  1333. const buttonsDiv = document.querySelectorAll('.buttons');
  1334.  
  1335. if (buttonsDiv.length > 0) {
  1336. //console.log('Buttons div already exists.');
  1337. } else if (!document.body.contains(loadMangaButton)) {
  1338. document.body.appendChild(loadMangaButton);
  1339.  
  1340. loadMangaButton.addEventListener('click', async function () {
  1341. const mangaId = extractMangaId(window.location.href);
  1342. if (mangaId) {
  1343. loadMangaImages(); // Load the manga images first
  1344.  
  1345. // Check if there's a saved position for the manga
  1346. const savedPosition = await retrieveData(mangaId);
  1347. if (savedPosition) {
  1348. const savedPage = savedPosition.pageNum;
  1349. if (savedPage && (savedPage === totalPages || savedPage + 1 === totalPages)) {
  1350. await GM.deleteValue(mangaId);
  1351. console.log(`Saved position deleted for ${mangaId} since it's equal to total pages.`);
  1352. } else {
  1353. showPopupForSavedPosition("Do you want to load your last saved position?", async () => {
  1354. await loadSavedPosition(mangaId);
  1355. }, {
  1356. confirmText: 'Yes',
  1357. cancelText: 'No',
  1358. duration: 10000
  1359. });
  1360. }
  1361. } else {
  1362. console.log('No saved position found for manga ID:', mangaId);
  1363. }
  1364. }
  1365. loadMangaButton.remove();
  1366. });
  1367. }
  1368. }
  1369. })();
  1370.  
  1371.  
  1372. //---------------------------**Continue Reading**---------------------------------
  1373.  
  1374. // Function to add the Continue Reading button to the menu
  1375. function addContinueReadingButton() {
  1376. // Create the Continue Reading button
  1377. const continueReadingButtonHtml = `
  1378. <li>
  1379. <a href="/continue_reading/">
  1380. <i class="fa fa-arrow-right"></i>
  1381. Continue Reading
  1382. </a>
  1383. </li>
  1384. `;
  1385. const continueReadingButton = $(continueReadingButtonHtml);
  1386.  
  1387. // Append the Continue Reading button to the dropdown menu and the left menu
  1388. const dropdownMenu = $('ul.dropdown-menu');
  1389. dropdownMenu.append(continueReadingButton);
  1390.  
  1391. const menu = $('ul.menu.left');
  1392. menu.append(continueReadingButton);
  1393. }
  1394.  
  1395. // Call the function to add the Continue Reading button
  1396. addContinueReadingButton();
  1397.  
  1398. // Handle continue_reading page
  1399. if (window.location.href.includes('/continue_reading')) {
  1400. console.log('Continue reading page detected');
  1401. // Remove 404 Not Found elements
  1402. const notFoundHeading = document.querySelector('h1');
  1403. if (notFoundHeading && notFoundHeading.textContent === '404 – Not Found') {
  1404. notFoundHeading.remove();
  1405. }
  1406. const notFoundMessage = document.querySelector('p');
  1407. if (notFoundMessage && notFoundMessage.textContent === "Looks like what you're looking for isn't here.") {
  1408. notFoundMessage.remove();
  1409. }
  1410.  
  1411. // Add custom CSS for better styling with dark theme
  1412. const customCSS = document.createElement('style');
  1413. customCSS.textContent = `
  1414. body {
  1415. background-color: #2c2c2c;
  1416. color: #f1f1f1;
  1417. margin: 0;
  1418. padding: 0;
  1419. }
  1420. .continue-reading-container {
  1421. width: 95%;
  1422. max-width: 1200px;
  1423. margin: 20px auto;
  1424. font-family: Arial, sans-serif;
  1425. padding: 20px;
  1426. overflow-x: hidden;
  1427. overflow-y: auto;
  1428. /*max-height: 100vh; /* Prevents it from exceeding the screen height */
  1429. }
  1430. h1.continue-reading-title {
  1431. text-align: center;
  1432. color: #ed2553;
  1433. margin-bottom: 20px;
  1434. }
  1435. table.manga-table {
  1436. width: 100%;
  1437. border-collapse: collapse;
  1438. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
  1439. background-color: #3c3c3c;
  1440. border-radius: 5px;
  1441. overflow: hidden;
  1442. box-sizing: border-box;
  1443. table-layout: fixed;
  1444. }
  1445. .manga-table th {
  1446. background-color: #ed2553;
  1447. color: white;
  1448. padding: 12px;
  1449. text-align: left;
  1450. }
  1451. .manga-table td {
  1452. padding: 12px;
  1453. border-bottom: 1px solid #4c4c4c;
  1454. word-wrap: break-word;
  1455. vertical-align: top;
  1456. }
  1457. /* Adjust column widths to optimize vertical layout */
  1458. .manga-table th:nth-child(1),
  1459. .manga-table td:nth-child(1) {
  1460. width: 30%;
  1461. }
  1462. .manga-table th:nth-child(2),
  1463. .manga-table td:nth-child(2) {
  1464. width: 30%;
  1465. }
  1466. .manga-table th:nth-child(3),
  1467. .manga-table td:nth-child(3) {
  1468. width: 30%;
  1469. }
  1470. .manga-table th:nth-child(4),
  1471. .manga-table td:nth-child(4) {
  1472. width: 30%;
  1473. }
  1474. .manga-table th:nth-child(5),
  1475. .manga-table td:nth-child(5) {
  1476. width: 30%;
  1477. text-align: center;
  1478. }
  1479. .manga-table tr:hover {
  1480. background-color: #4c4c4c;
  1481. }
  1482. .manga-table a {
  1483. color: #ed2553;
  1484. text-decoration: none;
  1485. font-weight: bold;
  1486. }
  1487. .manga-table a:hover {
  1488. text-decoration: underline;
  1489. }
  1490. .manga-title {
  1491. display: flex;
  1492. align-items: flex-start;
  1493. flex-wrap: wrap;
  1494. }
  1495. .manga-cover {
  1496. width: 50px;
  1497. height: 70px;
  1498. margin-right: 10px;
  1499. margin-bottom: 5px;
  1500. object-fit: cover;
  1501. border-radius: 3px;
  1502. }
  1503. .manga-title a {
  1504. display: inline-block;
  1505. /* Allow title to wrap properly */
  1506. word-break: break-word;
  1507. overflow-wrap: break-word;
  1508. width: 100%;
  1509. max-height: 100px;
  1510. overflow: hidden;
  1511. text-overflow: ellipsis;
  1512. display: -webkit-box;
  1513. -webkit-line-clamp: 4; /* Number of lines to show */
  1514. -webkit-box-orient: vertical;
  1515. }
  1516. .progress-bar-container {
  1517. width: 100%;
  1518. background-color: #555;
  1519. height: 8px;
  1520. border-radius: 4px;
  1521. margin-top: 5px;
  1522. }
  1523. .progress-bar {
  1524. height: 100%;
  1525. border-radius: 4px;
  1526. background-color: #ed2553;
  1527. }
  1528. .language-tag {
  1529. display: inline-block;
  1530. padding: 3px 8px;
  1531. border-radius: 3px;
  1532. background-color: #555;
  1533. font-size: 12px;
  1534. text-transform: capitalize;
  1535. }
  1536. .continue-button {
  1537. display: inline-block;
  1538. padding: 6px 12px;
  1539. background-color: #ed2553;
  1540. color: white !important;
  1541. border-radius: 4px;
  1542. text-align: center;
  1543. transition: background-color 0.2s;
  1544. }
  1545. .continue-button:hover {
  1546. background-color: #c91c45;
  1547. text-decoration: none !important;
  1548. }
  1549. .loading-indicator {
  1550. text-align: center;
  1551. margin: 20px 0;
  1552. font-size: 16px;
  1553. color: #ed2553;
  1554. }
  1555. .img-error {
  1556. border: 2px solid #ed2553;
  1557. position: relative;
  1558. }
  1559. .remove-button {
  1560. display: flex;
  1561. align-items: center;
  1562. justify-content: center;
  1563. background-color: white;
  1564. color: #ed2553; /* Initial red icon */
  1565. border: 2px solid #ed2553; /* Red border */
  1566. border-radius: 50%;
  1567. width: 30px;
  1568. height: 30px;
  1569. cursor: pointer;
  1570. transition: background-color 0.2s, color 0.2s, transform 0.1s, box-shadow 0.2s;
  1571. font-size: 14px;
  1572. font-weight: bold;
  1573. box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
  1574. margin: 0 auto;
  1575. }
  1576.  
  1577. .remove-button:hover {
  1578. background-color: #ed2553; /* Turns red */
  1579. color: white; /* Icon turns white */
  1580. box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
  1581. transform: scale(1.1);
  1582. }
  1583.  
  1584. .remove-button:active {
  1585. transform: scale(0.95);
  1586. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
  1587. }
  1588. /* Responsive adjustments for smaller screens */
  1589. @media (max-width: 768px) {
  1590. .continue-reading-container {
  1591. width: 98%;
  1592. padding: 10px;
  1593. }
  1594. .manga-table th,
  1595. .manga-table td {
  1596. padding: 8px 6px;
  1597. }
  1598. .manga-cover {
  1599. width: 40px;
  1600. height: 56px;
  1601. }
  1602. .continue-button {
  1603. padding: 4px 8px;
  1604. font-size: 14px;
  1605. }
  1606. /* For the continue button column */
  1607. .manga-table td:nth-child(4) {
  1608. position: relative;
  1609. vertical-align: middle;
  1610. }
  1611. /* Align the continue button to the bottom */
  1612. .manga-table td:nth-child(4) .continue-button {
  1613. position: relative;
  1614. bottom: 8px;
  1615. left: 6px;
  1616. }
  1617. }
  1618. `;
  1619. document.head.appendChild(customCSS);
  1620.  
  1621. // Create container element
  1622. const container = document.createElement('div');
  1623. container.className = 'continue-reading-container';
  1624. document.body.appendChild(container);
  1625. // Add title
  1626. const title = document.createElement('h1');
  1627. title.className = 'continue-reading-title';
  1628. title.textContent = 'Continue Reading';
  1629. container.appendChild(title);
  1630. // Add loading indicator
  1631. const loadingIndicator = document.createElement('div');
  1632. loadingIndicator.className = 'loading-indicator';
  1633. loadingIndicator.textContent = 'Loading your manga collection...';
  1634. container.appendChild(loadingIndicator);
  1635.  
  1636. // Implement the continue reading page
  1637. const mangaList = [];
  1638. // Array of possible subdomains to try
  1639. const subdomains = ['t3', 't', 't1', 't2', 't4', 't5', 't7'];
  1640. // Helper function to check if an image URL exists
  1641. function checkImageExists(url) {
  1642. return new Promise((resolve, reject) => {
  1643. const img = new Image();
  1644. img.onload = () => resolve(true);
  1645. img.onerror = () => resolve(false);
  1646. img.src = url;
  1647. });
  1648. }
  1649.  
  1650. // Helper function to find a working image URL
  1651. async function findWorkingImageUrl(mediaId) {
  1652. for (const subdomain of subdomains) {
  1653. // Try both webp and png formats
  1654. const webpUrl = `https://${subdomain}.nhentai.net/galleries/${mediaId}/cover.webp`;
  1655. const pngUrl = `https://${subdomain}.nhentai.net/galleries/${mediaId}/cover.png`;
  1656. const jpgUrl = `https://${subdomain}.nhentai.net/galleries/${mediaId}/cover.jpg`;
  1657.  
  1658. console.log(`Trying cover image URL: ${webpUrl}`);
  1659. const webpExists = await checkImageExists(webpUrl);
  1660. if (webpExists) {
  1661. console.log(`Found working URL: ${webpUrl}`);
  1662. return webpUrl;
  1663. }
  1664.  
  1665. console.log(`Trying cover image URL: ${pngUrl}`);
  1666. const pngExists = await checkImageExists(pngUrl);
  1667. if (pngExists) {
  1668. console.log(`Found working URL: ${pngUrl}`);
  1669. return pngUrl;
  1670. }
  1671.  
  1672. console.log(`Trying cover image URL: ${jpgUrl}`);
  1673. const jpgExists = await checkImageExists(jpgUrl);
  1674. if (jpgExists) {
  1675. console.log(`Found working URL: ${jpgUrl}`);
  1676. return jpgUrl;
  1677. }
  1678. }
  1679. // If all fail, return the default with t3 subdomain as fallback
  1680. return `https://t3.nhentai.net/galleries/${mediaId}/cover.jpg`;
  1681. }
  1682.  
  1683. // Function to create and display the table
  1684. function displayMangaTable() {
  1685. if (mangaList.length === 0) {
  1686. loadingIndicator.textContent = 'No manga found in your collection.';
  1687. return;
  1688. }
  1689. // Sort the manga list by most recently read (highest page number to lowest)
  1690. mangaList.sort((a, b) => b.currentPage - a.currentPage);
  1691. console.log('Sorted manga list:', mangaList);
  1692.  
  1693. // Create a table to display the manga list
  1694. const table = document.createElement('table');
  1695. table.className = 'manga-table';
  1696. table.innerHTML = `
  1697. <thead>
  1698. <tr>
  1699. <th>Manga Title</th>
  1700. <th>Progress</th>
  1701. <th>Language</th>
  1702. <th>Action</th>
  1703. <th>Remove</th>
  1704. </tr>
  1705. </thead>
  1706. <tbody></tbody>
  1707. `;
  1708. const tbody = table.querySelector('tbody');
  1709.  
  1710. // Add each manga to the table
  1711. mangaList.forEach(manga => {
  1712. console.log('Adding manga to table:', manga);
  1713. const row = document.createElement('tr');
  1714. // Calculate progress percentage
  1715. const progressPercent = (manga.currentPage / manga.pages) * 100;
  1716. row.innerHTML = `
  1717. <td>
  1718. <div class="manga-title">
  1719. <img class="manga-cover" src="${manga.coverImageUrl}" alt="Cover" onerror="this.classList.add('img-error')">
  1720. <a href="/g/${manga.id}/" title="${manga.title}">${manga.title}</a>
  1721. </div>
  1722. </td>
  1723. <td>
  1724. <div>Page ${manga.currentPage} of ${manga.pages}</div>
  1725. <div class="progress-bar-container">
  1726. <div class="progress-bar" style="width: ${progressPercent}%"></div>
  1727. </div>
  1728. </td>
  1729. <td><span class="language-tag">${manga.languageDisplay}</span></td>
  1730. <td><a href="/g/${manga.id}/${manga.currentPage}/" class="continue-button" onclick="localStorage.setItem('redirected', 'true');">Continue Reading</a></td>
  1731. <td><button class="remove-button" data-id="${manga.id}">X</button></td>
  1732. `;
  1733. tbody.appendChild(row);
  1734.  
  1735. // Remove manga entry from GM storage when 'X' button is clicked
  1736. row.querySelector('.remove-button').addEventListener('click', async function() {
  1737. const mangaId = this.getAttribute('data-id');
  1738. console.log(`Removing manga ID: ${mangaId}`);
  1739.  
  1740. // Remove from GM storage
  1741. await GM.deleteValue(mangaId);
  1742. await GM.deleteValue(`metadata_${mangaId}`);
  1743. console.log(`Manga ID ${mangaId} removed from GM storage`);
  1744.  
  1745. // Remove row from table
  1746. row.remove();
  1747. });
  1748.  
  1749. // Remove loading indicator and add the table
  1750. loadingIndicator.remove();
  1751. container.appendChild(table);
  1752. console.log('Table added to page');
  1753.  
  1754.  
  1755. // Handle image loading errors and try alternative subdomains
  1756. const imgElement = row.querySelector('.manga-cover');
  1757. imgElement.addEventListener('error', async function() {
  1758. console.log(`Image failed to load: ${manga.coverImageUrl}`);
  1759. // Extract media ID from the URL
  1760. const urlParts = manga.coverImageUrl.split('/');
  1761. const mediaId = urlParts[urlParts.length - 2];
  1762. // Find a working URL
  1763. const newUrl = await findWorkingImageUrl(mediaId);
  1764. if (newUrl !== manga.coverImageUrl) {
  1765. console.log(`Updating image URL from ${manga.coverImageUrl} to ${newUrl}`);
  1766. this.src = newUrl;
  1767. // Update the cached metadata with the working URL
  1768. const metadataKey = `metadata_${manga.id}`;
  1769. GM.getValue(metadataKey, null).then(cachedMetadata => {
  1770. if (cachedMetadata) {
  1771. const metadata = JSON.parse(cachedMetadata);
  1772. metadata.coverImageUrl = newUrl;
  1773. GM.setValue(metadataKey, JSON.stringify(metadata))
  1774. .then(() => console.log(`Updated cached metadata with new URL for manga ID: ${manga.id}`));
  1775. }
  1776. });
  1777. }
  1778. });
  1779. });
  1780.  
  1781. // Remove loading indicator and add the table
  1782. loadingIndicator.remove();
  1783. container.appendChild(table);
  1784. console.log('Table added to page');
  1785. }
  1786. // Function to fetch manga data from API and save it to GM.setValue
  1787. async function fetchAndSaveMangaData(mangaId, pageNum) {
  1788. const metadataKey = `metadata_${mangaId}`;
  1789. // Try to get cached metadata first
  1790. const cachedMetadata = await GM.getValue(metadataKey, null);
  1791. if (cachedMetadata) {
  1792. console.log(`Using cached metadata for manga ID: ${mangaId}`);
  1793. const metadata = JSON.parse(cachedMetadata);
  1794. mangaList.push({
  1795. id: mangaId,
  1796. title: metadata.title,
  1797. coverImageUrl: metadata.coverImageUrl,
  1798. languageDisplay: metadata.languageDisplay,
  1799. pages: metadata.pages,
  1800. currentPage: pageNum,
  1801. });
  1802. // Check if we have all manga data and display the table
  1803. checkAndDisplayTable();
  1804. return;
  1805. }
  1806. // If no cached data, fetch from API
  1807. console.log(`Fetching metadata for manga ID: ${mangaId}`);
  1808. try {
  1809. const response = await fetch(`https://nhentai.net/api/gallery/${mangaId}`);
  1810. const data = await response.json();
  1811. if (data) {
  1812. console.log('Fetched manga data:', data);
  1813. const mangaTitle = data.title.english;
  1814. const mediaId = data.media_id;
  1815. // Get a working cover image URL with appropriate subdomain
  1816. const coverImageUrl = await findWorkingImageUrl(mediaId);
  1817. // Determine which language to display
  1818. let languageDisplay = 'Unknown';
  1819. const languages = data.tags.filter(tag => tag.type === 'language').map(tag => tag.name.toLowerCase());
  1820. if (languages.includes('english')) {
  1821. languageDisplay = 'English';
  1822. } else if (languages.includes('translated') && languages.length === 1) {
  1823. languageDisplay = 'English';
  1824. } else if (languages.includes('translated') && languages.length > 1) {
  1825. // Exclude 'translated' and show other language(s)
  1826. const otherLanguages = languages.filter(lang => lang !== 'translated');
  1827. languageDisplay = otherLanguages.length > 0 ? otherLanguages.map(lang => lang.charAt(0).toUpperCase() + lang.slice(1)).join(', ') : 'Unknown';
  1828. } else {
  1829. languageDisplay = languages.map(lang => lang.charAt(0).toUpperCase() + lang.slice(1)).join(', ');
  1830. }
  1831. const pages = data.num_pages;
  1832. // Create metadata object to cache
  1833. const metadata = {
  1834. title: mangaTitle,
  1835. coverImageUrl: coverImageUrl,
  1836. languageDisplay: languageDisplay,
  1837. pages: pages,
  1838. lastUpdated: Date.now()
  1839. };
  1840. // Save metadata to GM storage
  1841. await GM.setValue(metadataKey, JSON.stringify(metadata));
  1842. console.log(`Saved metadata for manga ID: ${mangaId}`);
  1843. mangaList.push({
  1844. id: mangaId,
  1845. title: mangaTitle,
  1846. coverImageUrl: coverImageUrl,
  1847. languageDisplay: languageDisplay,
  1848. pages: pages,
  1849. currentPage: pageNum,
  1850. });
  1851. // Check if we have all manga data and display the table
  1852. checkAndDisplayTable();
  1853. } else {
  1854. console.log('No data found for manga ID:', mangaId);
  1855. checkAndDisplayTable();
  1856. }
  1857. } catch (error) {
  1858. console.error('Error fetching manga data:', error);
  1859. checkAndDisplayTable();
  1860. }
  1861. }
  1862. // Counter to track pending fetch operations
  1863. let pendingFetches = 0;
  1864. let totalMangaCount = 0;
  1865. // Function to check if all data is fetched and display the table
  1866. function checkAndDisplayTable() {
  1867. pendingFetches--;
  1868. loadingIndicator.textContent = `Loading your manga collection... (${totalMangaCount - pendingFetches}/${totalMangaCount})`;
  1869. if (pendingFetches <= 0) {
  1870. displayMangaTable();
  1871. }
  1872. }
  1873.  
  1874.  
  1875. // Function to delete completed manga
  1876. async function deleteCompletedManga() {
  1877. const allKeys = await GM.listValues();
  1878. console.log('All keys:', allKeys);
  1879. // Get all manga IDs (numerical keys)
  1880. const mangaIds = allKeys.filter(key => key.match(/^\d+$/));
  1881. // Process each manga
  1882. for (const mangaId of mangaIds) {
  1883. console.log('Processing manga ID:', mangaId);
  1884. const mangaData = await GM.getValue(mangaId);
  1885. const mangaDataObject = JSON.parse(mangaData);
  1886. const pagesRead = mangaDataObject.pageNum;
  1887. const metadataKey = `metadata_${mangaId}`;
  1888. const metadata = await GM.getValue(metadataKey);
  1889. const metadataObject = JSON.parse(metadata);
  1890. const totalPages = metadataObject.pages;
  1891. // Check if manga is completed (one less than or equal to total pages)
  1892. if (pagesRead >= totalPages - 1) {
  1893. console.log(`Deleting completed manga ID: ${mangaId}`);
  1894. await GM.deleteValue(mangaId);
  1895. await GM.deleteValue(metadataKey);
  1896. }
  1897. }
  1898. }
  1899.  
  1900.  
  1901. // Main function to load manga
  1902. async function getStoredManga() {
  1903. const mangaIds = [];
  1904. const allValues = {};
  1905. for (const key of await GM.listValues()) {
  1906. const value = await GM.getValue(key);
  1907. allValues[key] = value;
  1908. if (key.match(/^\d+$/)) {
  1909. mangaIds.push(key);
  1910. }
  1911. }
  1912. console.log('All values:', allValues);
  1913. totalMangaCount = mangaIds.length;
  1914. pendingFetches = totalMangaCount;
  1915. if (totalMangaCount === 0) {
  1916. loadingIndicator.textContent = 'No manga found in your collection.';
  1917. return;
  1918. }
  1919. // Process each manga
  1920. mangaIds.forEach(mangaId => {
  1921. console.log('Processing manga ID:', mangaId);
  1922. const mangaData = JSON.parse(allValues[mangaId]);
  1923. fetchAndSaveMangaData(mangaId, mangaData.pageNum);
  1924. });
  1925. }
  1926. deleteCompletedManga();
  1927. // Start the process
  1928. getStoredManga();
  1929. }
  1930.  
  1931. //---------------------------**Continue Reading**---------------------------------
  1932.