- // ==UserScript==
- // @name Nhentai Manga Loader
- // @namespace http://www.nhentai.net
- // @version 6.0.11
- // @author longkidkoolstar
- // @description Loads nhentai manga chapters into one page in a long strip format with image scaling, click events, and a dark mode for reading.
- // @match https://nhentai.net/*
- // @require https://code.jquery.com/jquery-3.6.0.min.js
- // @icon https://i.imgur.com/S0x03gs.png
- // @grant GM.getValue
- // @grant GM.setValue
- // @grant GM.deleteValue
- // @grant GM.listValues
- // @license MIT
- // @noframes
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- let loadedPages = 0; // Track loaded pages
- let totalPages = 0; // Track total pages
- let loadingImages = 0; // Track loading images
- let totalImages = 0; // Track total images
- let freshloadedcache = false;
- const mangaId = extractMangaId(window.location.href);
-
- // Add this new function to handle jumping to pages
- function handleJumpToPage(input) {
- const targetPage = parseInt(input.value);
- if (isNaN(targetPage) || targetPage < 1 || targetPage > totalPages) {
- alert(`Please enter a valid page number between 1 and ${totalPages}`);
- return;
- }
-
- const pageContainers = document.querySelectorAll('.manga-page-container');
- const targetContainer = Array.from(pageContainers).find(container => {
- const img = container.querySelector('img');
- return img && parseInt(img.alt.replace('Page ', '')) === targetPage;
- });
-
- if (targetContainer) {
- // Page is loaded, scroll to it
- if (/Mobi/i.test(navigator.userAgent)) {
- // Get the offset from the top of the document instead of viewport
- const offsetTop = targetContainer.offsetTop; // Add 5px to the top offset
- // Scroll to the absolute position
- window.scrollTo({
- top: offsetTop,
- left: 0,
- behavior: 'instant' // Use 'instant' for consistent behavior
- });
- } else {
- targetContainer.scrollIntoView({ behavior: 'smooth' });
- }
- } else {
- // Page not loaded, redirect to it
- const mangaId = extractMangaId(window.location.href);
- loadSpecificPage(targetPage, mangaId);
- }
-
- // Clear the input after jumping
- input.value = '';
- }
-
-
- (async () => {
- const value = JSON.parse(localStorage.getItem('redirected'));
- if (value === null) {
- localStorage.setItem('redirected', JSON.stringify(false)); // Flag to track if the page has been redirected
- }
- })();
-
- // Helper to create custom style sheets for elements
- function addCustomStyles() {
- const style = document.createElement('style');
- style.innerHTML = `
- #manga-container {
- max-width: 100vw;
- margin: 0 auto;
- padding: 0;
- }
- .manga-page-container {
- position: relative;
- display: block;
- margin: 0;
- }
- .manga-page-container img {
- max-width: 100%;
- display: block;
- margin: 3px auto;
- border-radius: 0;
- transition: all 0.3s ease;
- box-shadow: none;
- }
- .ml-counter {
- background-color: #222;
- color: white;
- border-radius: 10px;
- width: 40px;
- margin-left: auto;
- margin-right: auto;
- margin-top: -8.8px;
- padding-left: 5px;
- padding-right: 5px;
- border: 1px solid white;
- z-index: 100;
- position: relative;
- font-size: 9px;
- font-family: 'Open Sans', sans-serif;
- top: 4px;
- }
- .exit-btn {
- background-color: #e74c3c;
- color: white;
- padding: 5px 10px;
- font-size: 14px;
- border: none;
- border-radius: 8px;
- cursor: pointer;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- margin: 10px auto;
- display: block;
- text-align: center;
- }
- .exit-btn:hover {
- background-color: #c0392b;
- }
- .exit-btn:active {
- background-color: #a93226;
- }
- .ml-stats {
- position: fixed;
- bottom: 10px;
- right: 10px;
- background-color: rgba(0, 0, 0, 0.8);
- color: white;
- border-radius: 8px;
- padding: 3px;
- z-index: 1000;
- font-family: 'Open Sans', sans-serif;
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- }
- .ml-stats-content {
- display: flex;
- align-items: center;
- cursor: pointer;
- }
- .ml-button {
- cursor: pointer;
- margin-left: 5px;
- }
- .ml-box {
- display: none;
- background-color: #333;
- color: white;
- padding: 10px;
- border-radius: 5px;
- margin-top: 5px;
- width: 200px;
- }
- `;
- document.head.appendChild(style);
- }
-
- //------------------------------------------------------------------------------**Remove this when transfer over to Nhentai+**------------------------------------------------------------------------------
-
- let isPopupVisible = false; // Flag to track if the popup is visible
-
- function showPopupForSavedPosition(message, onConfirm, options = {}) {
- const existingPopup = document.getElementById('popup');
- if (existingPopup) {
- document.body.removeChild(existingPopup);
- }
-
- const popup = document.createElement('div');
- popup.id = 'popup';
- popup.innerHTML = `
- <div class="popup-content" role="alert">
- <p>${message}</p>
- <button class="confirm-btn">${options.confirmText || 'Yes'}</button>
- <button class="cancel-btn">${options.cancelText || 'No'}</button>
- </div>
- `;
- document.body.appendChild(popup);
-
- // Set the popup visibility flag
- isPopupVisible = true;
-
- // Add CSS styling for the popup
- const style = document.createElement('style');
- style.textContent = `
- #popup {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- background: rgba(0, 0, 0, 0.9);
- color: #fff;
- border-radius: 5px;
- z-index: 9999;
- padding: 15px;
- max-width: 300px;
- text-align: center;
- }
- .popup-content {
- position: relative;
- padding: 10px;
- }
- .confirm-btn,
- .cancel-btn {
- margin-top: 10px;
- background: none;
- border: none;
- color: #fff;
- font-size: 18px;
- cursor: pointer;
- transition: color 0.3s, transform 0.3s;
- margin: 0 5px; /* Space between buttons */
- }
- .confirm-btn:hover,
- .cancel-btn:hover {
- color: #ff0000; /* Change color on hover */
- transform: scale(1.1); /* Slightly enlarge on hover */
- }
- `;
- document.head.appendChild(style);
-
- // Handle confirmation button click
- document.querySelector('.confirm-btn').addEventListener('click', function() {
- document.body.removeChild(popup);
- document.head.removeChild(style);
- isPopupVisible = false; // Reset the flag when popup is closed
- if (onConfirm) onConfirm(); // Call the onConfirm callback
- });
-
- // Handle cancel button click
- document.querySelector('.cancel-btn').addEventListener('click', function() {
- document.body.removeChild(popup);
- document.head.removeChild(style);
- isPopupVisible = false; // Reset the flag when popup is closed
- });
-
- // Auto-close feature based on options
- const duration = options.duration || 10000; // Default to 10 seconds if not specified
- setTimeout(() => {
- if (document.body.contains(popup)) {
- document.body.removeChild(popup);
- document.head.removeChild(style);
- isPopupVisible = false; // Reset the flag when auto-closed
- }
- }, duration); // Use the specified duration
- }
-
-
-
- //------------------------------------------------------------------------------**Remove this when transfer over to Nhentai+**------------------------------------------------------------------------------
-
- // Function to extract manga ID from URL
- function extractMangaId(url) {
- const match = url.match(/\/g\/(\d+)/);
- return match ? match[1] : null;
- }
-
- function getCurrentPage(entry) {
- const pageElements = document.querySelectorAll('.manga-page-container');
- for (let i = 0; i < pageElements.length; i++) {
- if (entry.target === pageElements[i]) {
- const imgElement = pageElements[i].querySelector('img');
- const altText = imgElement.alt;
- const pageNumber = parseInt(altText.replace('Page ', ''));
- return pageNumber;
- }
- }
- return 1; // Default to page 1 if no current page is found
- }
-
-
- // Create the "Exit" button
- function createExitButton() {
- const button = document.createElement('button');
- button.textContent = 'Exit';
- button.className = 'exit-btn';
- return button;
- }
-
- // Add page counter below the image
- function addPageCounter(pageNumber) {
- const counter = document.createElement('div');
- counter.className = 'ml-counter';
- counter.textContent = `${pageNumber}`;
- return counter;
- }
-
- // Update stats display
- function updateStats() {
- const statsContainer = document.querySelector('.ml-stats-pages');
- const statsBox = document.querySelector('.ml-floating-msg');
- if (statsBox && !statsBox.querySelector('.jump-controls')) {
- statsBox.innerHTML = `<strong>Stats:</strong>
- <span class="ml-loading-images">${loadingImages} images loading</span>
- <span class="ml-total-images">${totalImages} images in chapter</span>
- <span class="ml-loaded-pages">${loadedPages} pages parsed</span>`;
-
- if (statsContainer) {
- statsContainer.textContent = `${loadedPages}/${totalPages} loaded`;
- }
- }
- }
-
- // Declare reloadMode at the top level
- let reloadMode = false; // Flag to track reload mode
-
- async function createStatsWindow() {
- const statsWindow = document.createElement('div');
- statsWindow.className = 'ml-stats';
-
- // Use a wrapper to keep the button and content aligned
- const statsWrapper = document.createElement('div');
- statsWrapper.style.display = 'flex';
- statsWrapper.style.alignItems = 'center'; // Center vertically
-
- const collapseButton = document.createElement('span');
- collapseButton.className = 'ml-stats-collapse';
- collapseButton.title = 'Hide stats';
- collapseButton.textContent = '>>';
- collapseButton.style.cursor = 'pointer';
- collapseButton.style.marginRight = '10px'; // Space between button and content
- collapseButton.addEventListener('click', async function() {
- contentContainer.style.display = contentContainer.style.display === 'none' ? 'block' : 'none';
- collapseButton.textContent = contentContainer.style.display === 'none' ? '<<' : '>>';
-
- // Save the collapse state
- await GM.setValue('statsCollapsed', contentContainer.style.display === 'none');
- });
-
- const contentContainer = document.createElement('div');
- contentContainer.className = 'ml-stats-content';
-
- const statsText = document.createElement('span');
- statsText.className = 'ml-stats-pages';
- statsText.textContent = `0/0 loaded`; // Initial stats
-
- const infoButton = document.createElement('i');
- infoButton.innerHTML = '<i class="fas fa-question-circle"></i>';
- infoButton.title = 'See userscript information and help';
- infoButton.style.marginLeft = '5px';
- infoButton.style.marginRight = '5px'; // Add space to the right
- infoButton.addEventListener('click', function() {
- 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.');
- });
-
- const moreStatsButton = document.createElement('i');
- moreStatsButton.innerHTML = '<i class="fas fa-chart-pie"></i>';
- moreStatsButton.title = 'See detailed page stats';
- moreStatsButton.style.marginRight = '5px';
- moreStatsButton.addEventListener('click', function() {
- const statsBox = document.querySelector('.ml-floating-msg');
-
- // If stats box is showing stats content, close it
- if (statsBox.style.display === 'block' && statsBox.querySelector('strong').textContent === 'Stats:') {
- statsBox.style.display = 'none';
- return;
- }
-
- // Show stats content
- statsBox.style.display = 'block';
- statsBox.innerHTML = `<strong>Stats:</strong>
- <span class="ml-loading-images">${loadingImages} images loading</span>
- <span class="ml-total-images">${totalImages} images in chapter</span>
- <span class="ml-loaded-pages">${loadedPages} pages parsed</span>`;
- });
-
- // Add new jump page button
- const jumpPageButton = document.createElement('i');
- jumpPageButton.innerHTML = '<i class="fas fa-search"></i>';
- jumpPageButton.title = 'Toggle jump to page';
- jumpPageButton.style.marginRight = '5px';
- jumpPageButton.addEventListener('click', function() {
- const statsBox = document.querySelector('.ml-floating-msg');
-
- // If stats box is showing jump page content, close it
- if (statsBox.style.display === 'block' && statsBox.querySelector('strong').textContent === 'Jump to Page') {
- statsBox.style.display = 'none';
- return;
- }
-
- // Show jump page content
- statsBox.style.display = 'block';
- statsBox.innerHTML = `<strong>Jump to Page</strong>
- <div class="jump-controls" style="display: flex; gap: 5px; margin: 5px 0;">
- <button class="jump-first">First</button>
- <input type="number" class="jump-input" min="1" max="${totalPages}" placeholder="1-${totalPages}">
- <button class="jump-last">Last</button>
- </div>
- <button class="load-saved-position">Load Saved Position</button>
- <button class="jump-go">Go</button>`;
-
- // Style the input and buttons
- const jumpInput = statsBox.querySelector('.jump-input');
- jumpInput.style.cssText = `
- flex: 2;
- width: 50px;
- background: #444;
- color: #fff;
- border: 1px solid #555;
- border-radius: 4px;
- padding: 2px 4px;
- `;
-
- // Style all buttons consistently
- const buttons = statsBox.querySelectorAll('button');
- buttons.forEach(button => {
- button.style.cssText = `
- background-color: #444;
- color: #fff;
- border: 1px solid #555;
- border-radius: 4px;
- padding: 2px 6px;
- cursor: pointer;
- transition: background-color 0.2s;
- width: 100%;
- margin-top: 5px;
- text-align: left;
- `;
- });
-
- // Special styling for First/Last buttons
- const firstLastButtons = statsBox.querySelectorAll('.jump-first, .jump-last');
- firstLastButtons.forEach(button => {
- button.style.cssText += `
- flex: 1;
- margin-top: 0;
- width: auto;
- `;
- });
-
- // Add event listeners
- const loadSavedPositionbtn = statsBox.querySelector('.load-saved-position')
- const jumpGo = statsBox.querySelector('.jump-go');
- const jumpFirst = statsBox.querySelector('.jump-first');
- const jumpLast = statsBox.querySelector('.jump-last');
-
- loadSavedPositionbtn.addEventListener('click', () => loadSavedPosition(mangaId));
- jumpGo.addEventListener('click', () => handleJumpToPage(jumpInput));
- jumpFirst.addEventListener('click', () => handleJumpToPage({ value: '1' }));
- jumpLast.addEventListener('click', () => handleJumpToPage({ value: totalPages.toString() }));
-
-
- });
-
- const refreshButton = document.createElement('i');
- refreshButton.innerHTML = '<i class="fas fa-sync-alt"></i>';
- refreshButton.title = 'Click an image to reload it.';
- refreshButton.addEventListener('click', function() {
- reloadMode = !reloadMode;
- refreshButton.style.color = reloadMode ? 'orange' : '';
- console.log(`Reload mode is now ${reloadMode ? 'enabled' : 'disabled'}.`);
- });
-
- // Add the mini exit button for refreshing the page
- const miniExitButton = document.createElement('button');
- miniExitButton.innerHTML = '<i class="fas fa-sign-out-alt"></i>'; // Font Awesome icon for sign out
- miniExitButton.title = 'Exit the Manga Loader';
- miniExitButton.style.marginLeft = '10px'; // Space between other buttons
- miniExitButton.style.backgroundColor = '#e74c3c'; // Red color for the button
- miniExitButton.style.color = '#fff';
- miniExitButton.style.border = 'none';
- miniExitButton.style.padding = '2px 5px';
- miniExitButton.style.borderRadius = '5px';
- miniExitButton.style.cursor = 'pointer';
-
- // Refresh the page when the button is clicked
- miniExitButton.addEventListener('click', function() {
- window.location.reload(); // Refresh the page
- });
-
- // Append all elements to the stats content container
- contentContainer.appendChild(statsText);
- contentContainer.appendChild(infoButton);
- contentContainer.appendChild(moreStatsButton);
- contentContainer.appendChild(jumpPageButton); // Add the new button
- contentContainer.appendChild(refreshButton);
- contentContainer.appendChild(miniExitButton);
-
- statsWrapper.appendChild(collapseButton);
- statsWrapper.appendChild(contentContainer);
- statsWindow.appendChild(statsWrapper);
-
- const statsBox = document.createElement('pre');
- statsBox.className = 'ml-box ml-floating-msg';
- statsBox.style.display = 'none'; // Initially hidden
-
- // Create the stats content
- const statsContent = `<strong>Stats:</strong>
- <span class="ml-loading-images">0 images loading</span>
- <span class="ml-total-images">0 images in chapter</span>
- <span class="ml-loaded-pages">0 pages parsed</span>`;
-
- statsBox.innerHTML = statsContent;
- statsWindow.appendChild(statsBox);
-
- // Check and set initial collapse state
- const collapsed = await GM.getValue('statsCollapsed', false);
- if (collapsed) {
- contentContainer.style.display = 'none';
- collapseButton.textContent = '<<'; // Change to indicate expanded state
- }
-
- // Add hover effect
- statsWindow.style.transition = 'opacity 0.3s';
- statsWindow.style.opacity = '0.6'; // Dimmed by default
-
- statsWindow.addEventListener('mouseenter', function() {
- statsWindow.style.opacity = '1'; // Fully visible on hover
- });
-
- statsWindow.addEventListener('mouseleave', function() {
- statsWindow.style.opacity = '0.6'; // Dim again on mouse leave
- });
-
- document.body.appendChild(statsWindow);
- }
-
-
-
-
- // Add the click event to images
- function addClickEventToImage(image) {
- image.addEventListener('click', function() {
- if (reloadMode) {
- const imgSrc = image.dataset.src || image.src;
- image.src = ''; // Clear the src to trigger reload
- setTimeout(() => {
- image.src = imgSrc; // Retry loading after clearing
- }, 100); // Short delay to ensure proper reload
- }
- });
- }
-
-
-
- // Function to hide specified elements
- function hideElements() {
- const elementsToHide = ['#image-container', '#content', 'nav'];
- elementsToHide.forEach(selector => {
- const element = document.querySelector(selector);
- if (element) {
- element.style.display = 'none';
- }
- });
- }
-
- // Add this at the top level to track image loading status
- const imageStatus = []; // Array to track the status of each image
-
- // Add an event listener to detect when the user scrolls
- window.addEventListener('scroll', logCurrentPage);
-
- // Variable to store the previous page
- let previousPage = 0;
-
- // Function to log the current page
- function logCurrentPage() {
- // Check if the URL matches the desired pattern
- if (!window.location.href.match(/^https:\/\/nhentai\.net\/g\//)) {
- return; // Exit if the URL is not correct
- }
-
- // Check if the download button exists
- if (document.querySelector("#download")) {
- return; // Exit if the download button exists
- }
-
- const currentPage = getCurrentVisiblePage();
- const totalPages = document.querySelectorAll('.manga-page-container').length;
-
- // Check if the Load Manga button exists
- const loadMangaButton = document.querySelector('.load-manga-btn');
- if (loadMangaButton) {
- return; // Exit if the Load Manga button exists
- }
-
- if ((currentPage === totalPages - 1 || currentPage === totalPages) && (!isPopupVisible || freshloadedcache)) {
- //console.log(`Current page: ${currentPage}`);
- previousPage = currentPage;
- if (currentPage >= totalPages - 1) deleteMangaFromStorage();
- }
- }
-
- function getCurrentVisiblePage() {
- const pageContainers = document.querySelectorAll('.manga-page-container');
- let visiblePage = 0;
- const totalPages = pageContainers.length;
-
- // No pages found
- if (totalPages === 0) {
- // console.warn('No page containers found.');
- return visiblePage;
- }
-
- // Determine if device is mobile or desktop based on screen width
- const isMobile = window.innerWidth <= 768; // Common breakpoint for mobile
-
- // Use different thresholds based on device type
- const visibilityThreshold = isMobile ? 70 : 25; // Lower threshold for desktop
-
- pageContainers.forEach((container, index) => {
- const img = container.querySelector('img');
- if (img && img.alt) {
- const pageNumber = parseInt(img.alt.replace('Page ', ''), 10);
- const rect = img.getBoundingClientRect();
- const pageHeight = rect.bottom - rect.top;
- const visibleHeight = Math.min(window.innerHeight, rect.bottom) - Math.max(0, rect.top);
- const visiblePercentage = (visibleHeight / pageHeight) * 100;
-
- if (visiblePercentage >= visibilityThreshold) {
- visiblePage = pageNumber;
- }
-
- // Keep the last page logic
- if (index + 1 === totalPages && visiblePercentage >= 10) {
- visiblePage = totalPages;
- }
- }
- });
-
- // Fallback logic remains the same
- if (visiblePage === 0) {
- const currentPageMatch = window.location.pathname.match(/\/g\/\d+\/(\d+)/);
- if (currentPageMatch) {
- visiblePage = parseInt(currentPageMatch[1], 10);
- }
- }
-
- //console.log("Current visible page determined:", visiblePage);
- return visiblePage;
- }
-
-
- // Function to delete manga from storage
- function deleteMangaFromStorage() {
- const mangaId = window.location.pathname.match(/\/g\/(\d+)/)[1];
- GM.deleteValue(mangaId); // Delete the manga entry
-
- // Check if metadata exists before attempting to delete it
- GM.getValue(`metadata_${mangaId}`).then(metadata => {
- if (metadata) {
- GM.deleteValue(`metadata_${mangaId}`); // Delete the associated metadata
- console.log(`Metadata for manga ${mangaId} deleted from storage`);
- } else {
- console.log(`No metadata found for manga ${mangaId}, skipping deletion`);
- }
- });
-
- console.log(`Manga ${mangaId} deleted from storage`);
- }
-
- // Replace the addScrollListener function with the following code
- let previousPagex = 0; // Initialize previousPage at the top level
-
- window.addEventListener('scroll', async () => {
- const currentPage = getCurrentVisiblePage();
- //console.log("current page:", currentPage, "last page", previousPagex);
- // Only save the current page if the popup is not visible and the current page is greater than the previous page
- if (!isPopupVisible || freshloadedcache) {
- if (currentPage > previousPagex) {
- console.log(`Current page: ${currentPage}, Previous page: ${previousPagex}`);
- await saveCurrentPosition(mangaId, currentPage);
- previousPagex = currentPage; // Update previousPage to the current page
- }
- }
- });
- // Log the state of freshloadedcache every second
- setInterval(() => {
- // console.log(`Fresh loaded cache state: ${freshloadedcache}`);
- }, 1000);
-
-
- // Load all manga images with page separators and scaling
- function loadMangaImages(mangaId) {
- hideElements();
- createStatsWindow(); // Create the stats window
-
- const mangaContainer = document.createElement('div');
- mangaContainer.id = 'manga-container';
- document.body.appendChild(mangaContainer);
-
-
-
-
- const exitButtonTop = createExitButton();
- mangaContainer.appendChild(exitButtonTop);
-
- totalPages = parseInt(document.querySelector('.num-pages').textContent.trim());
- totalImages = totalPages; // Update total images for stats
- const initialPage = parseInt(window.location.href.match(/\/g\/\d+\/(\d+)/)[1]);
- let currentPage = initialPage;
-
- // Queue for tracking loading images
- const loadingQueue = [];
- const maxConcurrentLoads = /Mobi/.test(navigator.userAgent) ? 10 : 40; // Maximum number of concurrent image loads
-
- // Helper to create the page container with images
- function createPageContainer(pageNumber, imgSrc) {
- const container = document.createElement('div');
- container.className = 'manga-page-container';
-
- // Create the actual image element
- const img = document.createElement('img');
- img.src = ''; // Start with empty src to avoid loading it immediately
- img.dataset.src = imgSrc; // Store the actual src in data attribute
- img.alt = `Page ${pageNumber}`;
-
- // Add page counter below the image
- const pageCounter = addPageCounter(pageNumber);
-
- // Append the image and page counter
- container.appendChild(img);
- container.appendChild(pageCounter); // <-- Page number is shown here
-
- // Add exit button to the bottom of the last loaded page
- if (pageNumber === totalPages) {
- const exitButton = createExitButton();
- container.appendChild(exitButton);
- exitButton.addEventListener('click', () => {
- window.location.reload();
- })
- }
-
- // Track the image status
- imageStatus[pageNumber] = { src: imgSrc, loaded: false, attempts: 0 };
-
- // Error handling and event listeners
- addErrorHandlingToImage(img, imgSrc, pageNumber);
- addClickEventToImage(img);
- mangaContainer.appendChild(container);
-
- loadedPages++; // Increment loaded pages count
- updateStats(); // Update stats display
-
- observePageContainer(container); // Observe for lazy loading
-
- // Save scroll position as soon as page container is created
- const mangaId = extractMangaId(window.location.href);
- const currentPage = getCurrentVisiblePage(); // Get the current visible page number
- if (!isPopupVisible || freshloadedcache) {
- // console.log("load again");
- // saveCurrentPosition(mangaId, currentPage);
- }
-
-
- // Start loading the actual image
- img.src = imgSrc; // Set the src to load the image
-
- // Mark as loaded on load
- img.onload = () => {
- imageStatus[pageNumber].loaded = true; // Mark as loaded
- loadingImages--; // Decrement loading images count
- updateStats(); // Update loading images count
- };
-
- return container;
- }
-
-
-
-
-
-
-
- // Add a delay function
- function delay(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
-
- // Track if the app is online or offline
- let isOnline = navigator.onLine;
-
- // Add event listeners to detect connection state changes
- window.addEventListener('offline', () => {
- console.warn('You are offline. Pausing image loading.');
- isOnline = false;
- });
-
- window.addEventListener('online', () => {
- console.log('Back online. Resuming image loading.');
- isOnline = true;
- if (loadingQueue.length > 0) {
- processQueue(); // Resume processing the queue
- } else {
- // If queue is empty, manually trigger the next page load
- loadNextBatchOfImages(); // Load the next set of images if queue is empty
- }
- });
-
- // Load a single page with error handling, retry logic, and caching
- async function loadPage(pageNumber, pageUrl, retryCount = 0) {
- if (loadingImages >= maxConcurrentLoads || !isOnline) {
- return; // Exit if we're at max concurrent loads or offline
- }
-
- loadingImages++;
- updateStats(); // Update loading images count
-
- const mangaId = extractMangaId(pageUrl);
- if (!mangaId) {
- console.error(`Could not extract manga ID from URL: ${pageUrl}`);
- loadingImages--;
- updateStats();
- handleFailedImage(pageNumber);
- return;
- }
-
- // Check cache first
- const cachedImage = getImageFromCache(pageNumber, mangaId);
- if (cachedImage && cachedImage.mangaId === mangaId) {
- console.log(`Loading page ${pageNumber} from cache for manga ${mangaId}`);
- const pageContainer = createPageContainer(pageNumber, cachedImage.imgSrc);
- imageStatus[pageNumber].loaded = true; // Mark as loaded
-
- // Ensure position is saved for cached pages
- const currentPage = pageNumber;
- // console.log("load");
- // saveCurrentPosition(mangaId, currentPage); // Save the position for cached pages
-
-
-
- loadingImages--;
- updateStats(); // Update loading images count
-
- // Pre-fetch the next page if it's not the last page
- if (pageNumber < totalPages && cachedImage.nextLink) {
- loadingQueue.push({ pageNumber: pageNumber + 1, pageUrl: cachedImage.nextLink });
- processQueue(); // Check the queue
- }
- return;
- }
-
- try {
- const response = await fetch(pageUrl);
-
- if (response.status === 429) {
- if (retryCount < maxRetries) {
- console.warn(`Rate limit exceeded for page ${pageNumber}. Retrying in ${retryDelay} ms...`);
- await delay(retryDelay); // Wait before retrying
- loadPage(pageNumber, pageUrl, retryCount + 1); // Retry loading the same page
- return;
- } else {
- console.error(`Failed to load page ${pageNumber} after ${maxRetries} attempts.`);
- loadingImages--;
- updateStats(); // Update loading images count
- handleFailedImage(pageNumber); // Handle failed image loading
- return;
- }
- }
-
- const html = await response.text();
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
- const imgElement = doc.querySelector('#image-container > a > img');
- const nextLink = doc.querySelector('#image-container > a').href;
- const imgSrc = imgElement.getAttribute('data-src') || imgElement.src;
-
- // Save to cache
- saveImageToCache(pageNumber, imgSrc, nextLink, mangaId);
-
- const pageContainer = createPageContainer(pageNumber, imgSrc);
- imageStatus[pageNumber].loaded = true; // Mark as loaded
-
- loadingImages--;
- updateStats(); // Update loading images count
-
- // Pre-fetch the next page once the current one loads
- if (pageNumber < totalPages && nextLink) {
- loadingQueue.push({ pageNumber: pageNumber + 1, pageUrl: nextLink });
- processQueue(); // Check the queue
- }
- } catch (err) {
- loadingImages--;
- console.error(err);
- updateStats(); // Update loading images count
- handleFailedImage(pageNumber); // Handle failed image loading
- }
- }
-
- // In your processing queue, ensure a delay ONLY after 429 status
- async function processQueue() {
- while (loadingQueue.length > 0 && loadingImages < maxConcurrentLoads && isOnline) {
- const { pageNumber, pageUrl } = loadingQueue.shift(); // Get the next page to load
- loadPage(pageNumber, pageUrl); // Load it
- }
- }
-
- // Manually trigger the next batch of images if needed
- function loadNextBatchOfImages() {
- if (loadingQueue.length === 0 && isOnline) {
- const nextPageNumber = getNextPageNumber(); // Logic to get the next page number
- const nextPageUrl = getNextPageUrl(nextPageNumber); // Logic to get the next page URL
-
- if (nextPageUrl) {
- loadingQueue.push({ pageNumber: nextPageNumber, pageUrl: nextPageUrl });
- processQueue(); // Resume loading
- }
- }
- }
-
- // Configuration for retry logic
- const maxRetries = 5; // Maximum number of retries for rate limit
- const retryDelay = 5000; // Delay in milliseconds before retrying only on 429 status
-
-
-
-
-
- // Handle failed image loading attempts
- function handleFailedImage(pageNumber) {
- if (imageStatus[pageNumber]) {
- imageStatus[pageNumber].attempts++;
- if (imageStatus[pageNumber].attempts <= 3) { // Retry up to 3 times
- console.warn(`Retrying to load image for page ${pageNumber}...`);
- loadPage(pageNumber, document.querySelector(`#image-container > a`).href); // Reattempt loading the same page
- } else {
- console.error(`Failed to load image for page ${pageNumber} after 3 attempts.`);
- }
- }
- }
-
-
-
- const firstImageElement = document.querySelector('#image-container > a > img');
- const firstImgSrc = firstImageElement.getAttribute('data-src') || firstImageElement.src;
- createPageContainer(currentPage, firstImgSrc);
-
- const firstImageLink = document.querySelector('#image-container > a').href;
- loadingQueue.push({ pageNumber: currentPage + 1, pageUrl: firstImageLink }); // Add to queue
- processQueue(); // Start processing the queue
-
- // Observe all image containers for lazy loading
- observeAndPreloadImages(); // <-- Add this here to track and lazy-load images
-
- exitButtonTop.addEventListener('click', function() {
- window.location.reload();
- });
- }
-
- // Pre-load next few images while user scrolls
- function observeAndPreloadImages() {
- const observer = new IntersectionObserver((entries) => {
- entries.forEach((entry) => {
- if (entry.isIntersecting) {
- const imgElement = entry.target.querySelector('img');
- if (imgElement && imgElement.dataset.src) {
- imgElement.src = imgElement.dataset.src; // Load the image
- observer.unobserve(entry.target); // Stop observing after loading
-
- // Save the current position
- const mangaId = extractMangaId(window.location.href);
- const currentPage = getCurrentVisiblePage(entry); // Get the current page number
- if (!isPopupVisible || freshloadedcache) {
- //console.log("preload");
- // saveCurrentPosition(mangaId, currentPage);
- }
- }
- }
- });
- }, {
- rootMargin: '300px 0px', // Load images 300px before they appear
- threshold: 0.1
- });
-
- // Observe each image container
- const imageContainers = document.querySelectorAll('.manga-page-container');
- imageContainers.forEach((container) => observer.observe(container));
- }
-
- // Function to get image data from local storage
- function getImageFromCache(pageNumber, mangaId) {
- // console.log("freshloadedcache", freshloadedcache);
- freshloadedcache = true;
- setInterval(() => {
- //console.log("freshloadedcache", freshloadedcache);
- freshloadedcache = false;
- }, 3000)
- const cacheKey = `imagePage_${mangaId}_${pageNumber}`;
- const cachedData = localStorage.getItem(cacheKey);
- if (cachedData) {
- return JSON.parse(cachedData);
- }
- return null;
- }
-
- // Function to save image data to local storage
- function saveImageToCache(pageNumber, imgSrc, nextLink, mangaId) {
- const cacheKey = `imagePage_${mangaId}_${pageNumber}`;
- const cacheData = { imgSrc, nextLink, timestamp: Date.now(), mangaId };
- localStorage.setItem(cacheKey, JSON.stringify(cacheData));
- }
-
-
- function addErrorHandlingToImage(image, imgSrc, pageNumber) {
- const subdomains = ['i1', 'i2', 'i3', 'i4', 'i5', 'i7']; // Add the alternative subdomains here
- let currentSubdomainIndex = 0;
-
- function updateImageSource(newSrc) {
- image.src = newSrc;
- image.dataset.src = newSrc; // Update data-src attribute
- updateImageCache(newSrc);
- }
-
- function updateImageCache(newSrc) {
- const mangaId = extractMangaId(window.location.href);
- const cachedData = getImageFromCache(pageNumber, mangaId);
- if (cachedData) {
- cachedData.imgSrc = newSrc;
- saveImageToCache(pageNumber, newSrc, cachedData.nextLink, mangaId);
- console.log(`Updated cache for page ${pageNumber} with new URL: ${newSrc}`);
- }
- }
-
- image.onerror = function() {
- console.warn(`Failed to load image: ${imgSrc} on page ${pageNumber}. Retrying...`);
-
- if (!imageStatus[pageNumber].retryCount) {
- imageStatus[pageNumber].retryCount = 0;
- }
-
- if (imageStatus[pageNumber].retryCount < subdomains.length) {
- imageStatus[pageNumber].retryCount++;
-
- const newSubdomain = subdomains[currentSubdomainIndex];
- const newImgSrc = imgSrc.replace(/i\d/, newSubdomain);
-
- currentSubdomainIndex = (currentSubdomainIndex + 1) % subdomains.length;
- console.log(`Retrying with new subdomain: ${newSubdomain} for page ${pageNumber}`);
-
- setTimeout(() => {
- updateImageSource(newImgSrc);
- // Update the local storage cache for this page
- const mangaId = extractMangaId(window.location.href);
- const cachedData = getImageFromCache(pageNumber, mangaId);
- if (cachedData) {
- saveImageToCache(pageNumber, newImgSrc, cachedData.nextLink, mangaId);
- console.log(`Updated local storage cache for page ${pageNumber} with new URL: ${newImgSrc}`);
- }
- }, 1000);
- } else {
- console.error(`Failed to load image on page ${pageNumber} after multiple attempts.`);
- image.alt = `Failed to load page ${pageNumber}`;
- }
- };
-
- // Update cache even if image loads successfully from cache
- image.onload = function() {
- updateImageCache(image.src);
- };
- }
-
-
- // Create an IntersectionObserver to prioritize loading images that are in or near the viewport
- // Create an IntersectionObserver to prioritize loading images that are in or near the viewport
- const observer = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- if (entry.isIntersecting) {
- const imgElement = entry.target.querySelector('img');
- if (imgElement && imgElement.dataset.src) {
- imgElement.src = imgElement.dataset.src; // Load the image
- observer.unobserve(entry.target); // Stop observing after loading
-
- // Save the current scroll position as soon as the image starts loading
- const mangaId = extractMangaId(window.location.href);
- const currentPage = getCurrentVisiblePage(); // Get the current visible page number
- if (!isPopupVisible || freshloadedcache) {
- //console.log("intesect");
- // saveCurrentPosition(mangaId, currentPage);
- }
- }
-
- }
- });
- }, {
- rootMargin: '200px 0px', // Adjust for preloading images slightly outside the viewport
- threshold: 0.1 // Trigger loading when 10% of the image is in view
- });
-
- function observePageContainer(container) {
- observer.observe(container); // Observe each page container for lazy loading
- }
-
-
- addCustomStyles();
-
-
- // Compress data into a string format
- function compressData(data) {
- return JSON.stringify(data);
- }
-
- // Decompress data from string format
- function decompressData(data) {
- return JSON.parse(data);
- }
-
- async function storeData(mangaId, pageNum) {
- const existingData = await retrieveData(mangaId);
- const existingPageNum = existingData ? existingData.pageNum : 0;
-
- if (pageNum > existingPageNum) {
- const currentTime = Date.now();
- const data = { pageNum, lastAccessed: currentTime };
- const compressedData = compressData(data);
- await GM.setValue(mangaId, compressedData);
-
- // Manage storage size if it exceeds the limit
- await manageStorage();
- }
- }
-
- // Retrieve data from Tampermonkey storage
- async function retrieveData(mangaId) {
- const compressedData = await GM.getValue(mangaId, null);
- if (compressedData) {
- return decompressData(compressedData);
- }
- return null;
- }
-
- // Delete the least recently accessed data if the limit is reached
- async function manageStorage() {
- const MAX_ENTRIES = 52; // Limit to store 50 recent hentai
- const keys = await GM.listValues();
- if (keys.length > MAX_ENTRIES) {
- const entries = [];
- for (let key of keys) {
- const value = await GM.getValue(key);
- const data = decompressData(value);
- entries.push({ key, lastAccessed: data.lastAccessed });
- }
-
- // Sort by last accessed time, oldest first
- entries.sort((a, b) => a.lastAccessed - b.lastAccessed);
-
- // Remove the oldest entries until we're under the limit
- const excess = entries.length - MAX_ENTRIES;
- for (let i = 0; i < excess; i++) {
- await GM.deleteValue(entries[i].key);
- }
- }
- }
-
- let isRestoringPosition = false; // Flag to prevent overwriting saved position
-
-
-
- function getCurrentPageFromURL() {
- const match = window.location.pathname.match(/\/g\/\d+\/(\d+)\//);
- return match ? parseInt(match[1], 10) : 1; // Default to 1 if not found
- }
-
-
- async function loadSavedPosition(mangaId) {
- console.log(`Trying to load saved position for: ${mangaId}`);
-
- const savedData = await retrieveData(mangaId);
- console.log(`Saved data retrieved:`, savedData); // Log the retrieved data
-
- const savedPage = savedData.pageNum;
- if (savedPage && savedPage === totalPages) {
- await GM.deleteValue(mangaId);
- console.log(`Saved position deleted for ${mangaId} since it's equal to total pages.`);
- return;
- }
-
- if (savedData) {
- const savedPage = savedData.pageNum;
- console.log(`Saved page is: ${savedPage}`); // Log the saved page number
-
- const currentPage = getCurrentPageFromURL(); // Get current page from URL
- console.log(`Current page is: ${currentPage}`); // Log the current page number
-
- // Only proceed if the saved page is different from the current one
- if (savedPage && savedPage !== currentPage) {
- console.log(`Restoring to saved page: ${savedPage}`);
- isRestoringPosition = true; // Set the flag before restoring the position
-
- const pageContainers = document.querySelectorAll('.manga-page-container');
- console.log(`Total page containers loaded: ${pageContainers.length}`); // Log how many pages are loaded
-
- if (pageContainers.length > 0) {
- // Directly scroll to the saved page
- scrollToSavedPage(pageContainers, savedPage);
- } else {
- console.log(`Waiting for pages to load...`);
- waitForPageContainers(savedPage); // Use a MutationObserver to wait for containers
- }
- } else {
- console.log(`Not restoring saved position for ${mangaId}. Current page is the same as saved page.`);
- }
- } else {
- console.log(`No saved position found for ${mangaId}.`);
- }
-
- isRestoringPosition = false; // Reset the flag after restoring the position
- }
-
- function waitForPageContainers(savedPageWithOffset) {
- const observer = new MutationObserver((mutations, obs) => {
- const pageContainers = document.querySelectorAll('.manga-page-container');
- if (pageContainers.length >= savedPageWithOffset) {
- console.log(`Page containers are now loaded: ${pageContainers.length}`);
- obs.disconnect(); // Stop observing once the pages are loaded
- scrollToSavedPage(pageContainers, savedPageWithOffset); // Scroll to the saved page
- }
- });
-
- // Observe changes in the DOM (specifically looking for added nodes)
- observer.observe(document.body, {
- childList: true,
- subtree: true
- });
- }
-
- // Queue for specific page loading
- const specificPageQueue = [];
-
-
-
- // Function to load a specific page by redirecting to its URL
- async function loadSpecificPage(pageNumber) {
- const mangaId = extractMangaId(window.location.href); // Extract manga ID from current URL
- const pageUrl = `https://nhentai.net/g/${mangaId}/${pageNumber}/`; // Construct the URL for the specific page
-
- console.log(`Redirecting to page ${pageNumber} at URL: ${pageUrl}`);
-
- localStorage.setItem('redirected', 'true'); // Save the redirected state in local storage
- console.log(`Set redirected flag to true in storage.`); // Log confirmation of setting the flag
-
- window.location.href = pageUrl; // Redirect to the specific page URL
- }
-
- // Function to check if the page is redirected and load manga images
- async function checkRedirected() {
- const wasRedirected = JSON.parse(localStorage.getItem('redirected') || 'false'); // Retrieve the redirected state
-
- if (wasRedirected) {
- const mangaId = extractMangaId(window.location.href);
- console.log(`Loading manga images for manga ID: ${mangaId}`); // Log the manga ID
- loadMangaButton.remove(); // Remove the load manga button since we already did it
- loadMangaImages(mangaId); // Call loadMangaImages after redirection
- console.log(`Reset redirected flag to false in storage.`); // Log confirmation of resetting the flag
- localStorage.setItem('redirected', JSON.stringify(false)); // Reset the flag in storage
- }
- }
- // Call the function every second
- setInterval(checkRedirected, 1000);
-
-
-
-
-
- async function scrollToSavedPage(pageContainers, savedPage, savedImgSrc) {
- const currentPage = getCurrentPageFromURL(); // Get current page number from URL
- const savedPageIndex = savedPage - currentPage; // Calculate the effective saved page index
-
- console.log(`Current page: ${currentPage}, Adjusted index for saved page: ${savedPageIndex}`);
-
- // Check if the adjusted index is out of bounds
- if (savedPageIndex < 0 || savedPageIndex >= pageContainers.length) {
- console.warn(`Adjusted saved page index ${savedPageIndex} is out of bounds.`);
- console.log(`Page ${savedPage} is not loaded yet. Redirecting to its URL.`);
- loadSpecificPage(savedPage); // Redirect to the specific page
- return; // Exit early
- }
-
- const savedPageElement = pageContainers[savedPageIndex]; // Get the container for the saved page
- const img = savedPageElement.querySelector('img');
-
-
- // If the image is loaded
- if (img && img.complete) {
- console.log(`Image for page ${savedPage} loaded. Moving to it.`);
- if (/Mobi/i.test(navigator.userAgent)) {
- const rect = savedPageElement.getBoundingClientRect();
- window.scrollTo(rect.left, rect.top); // teleport on mobile
- } else {
- savedPageElement.scrollIntoView({ behavior: 'smooth' }); // scroll on desktop
- }
- } else {
- console.log(`Image for page ${savedPage} not loaded yet. Redirecting to its URL.`);
- loadSpecificPage(savedPage); // Redirect to the specific page
- }
- }
-
- //----------------------------------------------Make the second option later When in Main Script----------------------------------------------
-
- // If the image is loaded
- // if (img && img.complete) {
- // console.log(`Image for page ${savedPage} loaded. Scrolling to it.`);
- // savedPageElement.scrollIntoView({ behavior: 'smooth' });
- // }
-
-
- // If the image is loaded
- // if (img && img.complete) {
- // console.log(`Image for page ${savedPage} loaded. Scrolling to it.`);
- // savedPageElement.scrollIntoView({ behavior: 'smooth' });
- // }
-
- //----------------------------------------------Make the second option later When in Main Script----------------------------------------------
-
-
-
- function getNextPageUrl(pageNumber) {
- console.log(`Searching for URL for page ${pageNumber}`);
- const pageLink = document.querySelector(`#image-container a[href*='/g/'][href*='/${pageNumber}/']`);
- console.log(`Found page link: ${pageLink}`);
- if (pageLink) {
- return pageLink.href;
- } else {
- console.log(`No URL found for page ${pageNumber}`);
- return null;
- }
- }
-
-
-
-
- // Save the current position without checking for visibility// Save the current position based on the current page
- async function saveCurrentPosition(mangaId) {
- const totalPages = document.querySelectorAll('.manga-page-container').length;
- const currentPage = getCurrentVisiblePage(); // Get the current page number from the URL
-
- // Log the total pages and current page for debugging
- console.log(`Total pages loaded: ${totalPages}, trying to save position for page: ${currentPage}`);
-
- // Always save the position
- if (!isRestoringPosition) { // Only save if we are not restoring
- await storeData(mangaId, currentPage);
- console.log(`Position saved: Manga ID: ${mangaId}, Page: ${currentPage}`);
- } else {
- console.log(`Not saving position for Manga ID: ${mangaId} as we are restoring.`);
- }
- }
-
-
- // Periodically clean up storage
- manageStorage();
-
-
- window.loadMangaButton = document.createElement('button');
- loadMangaButton.textContent = 'Load Manga';
- loadMangaButton.className = 'load-manga-btn';
- loadMangaButton.style.position = 'fixed';
- loadMangaButton.style.bottom = '0';
- loadMangaButton.style.right = '0';
- loadMangaButton.style.padding = '5px';
- loadMangaButton.style.margin = '0 10px 10px 0';
- loadMangaButton.style.zIndex = '9999999999';
-
- if (window.location.href.startsWith("https://nhentai.net/g/")) {
- const buttonsDiv = document.querySelectorAll('.buttons');
-
- if (buttonsDiv.length > 0) {
- //console.log('Buttons div already exists.');
- } else if (!document.body.contains(loadMangaButton)) {
- document.body.appendChild(loadMangaButton);
-
- loadMangaButton.addEventListener('click', async function () {
- const mangaId = extractMangaId(window.location.href);
- if (mangaId) {
- loadMangaImages(); // Load the manga images first
-
- // Check if there's a saved position for the manga
- const savedPosition = await retrieveData(mangaId);
- if (savedPosition) {
- const savedPage = savedPosition.pageNum;
- if (savedPage && (savedPage === totalPages || savedPage + 1 === totalPages)) {
- await GM.deleteValue(mangaId);
- console.log(`Saved position deleted for ${mangaId} since it's equal to total pages.`);
- } else {
- showPopupForSavedPosition("Do you want to load your last saved position?", async () => {
- await loadSavedPosition(mangaId);
- }, {
- confirmText: 'Yes',
- cancelText: 'No',
- duration: 10000
- });
- }
- } else {
- console.log('No saved position found for manga ID:', mangaId);
- }
- }
- loadMangaButton.remove();
- });
- }
- }
-
-
-
- })();
-
-
- //---------------------------**Continue Reading**---------------------------------
-
- // Function to add the Continue Reading button to the menu
- function addContinueReadingButton() {
- // Create the Continue Reading button
- const continueReadingButtonHtml = `
- <li>
- <a href="/continue_reading/">
- <i class="fa fa-arrow-right"></i>
- Continue Reading
- </a>
- </li>
- `;
- const continueReadingButton = $(continueReadingButtonHtml);
-
- // Append the Continue Reading button to the dropdown menu and the left menu
- const dropdownMenu = $('ul.dropdown-menu');
- dropdownMenu.append(continueReadingButton);
-
- const menu = $('ul.menu.left');
- menu.append(continueReadingButton);
- }
-
- // Call the function to add the Continue Reading button
- addContinueReadingButton();
-
- // Handle continue_reading page
- if (window.location.href.includes('/continue_reading')) {
- console.log('Continue reading page detected');
-
- // Remove 404 Not Found elements
- const notFoundHeading = document.querySelector('h1');
- if (notFoundHeading && notFoundHeading.textContent === '404 – Not Found') {
- notFoundHeading.remove();
- }
-
- const notFoundMessage = document.querySelector('p');
- if (notFoundMessage && notFoundMessage.textContent === "Looks like what you're looking for isn't here.") {
- notFoundMessage.remove();
- }
-
- // Add custom CSS for better styling with dark theme
- const customCSS = document.createElement('style');
- customCSS.textContent = `
- body {
- background-color: #2c2c2c;
- color: #f1f1f1;
- margin: 0;
- padding: 0;
- }
-
- .continue-reading-container {
- width: 95%;
- max-width: 1200px;
- margin: 20px auto;
- font-family: Arial, sans-serif;
- padding: 20px;
- overflow-x: hidden;
- overflow-y: auto;
- /*max-height: 100vh; /* Prevents it from exceeding the screen height */
- }
-
- h1.continue-reading-title {
- text-align: center;
- color: #ed2553;
- margin-bottom: 20px;
- }
-
- table.manga-table {
- width: 100%;
- border-collapse: collapse;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
- background-color: #3c3c3c;
- border-radius: 5px;
- overflow: hidden;
- box-sizing: border-box;
- table-layout: fixed;
- }
-
- .manga-table th {
- background-color: #ed2553;
- color: white;
- padding: 12px;
- text-align: left;
- }
-
- .manga-table td {
- padding: 12px;
- border-bottom: 1px solid #4c4c4c;
- word-wrap: break-word;
- vertical-align: top;
- }
-
- /* Adjust column widths to optimize vertical layout */
- .manga-table th:nth-child(1),
- .manga-table td:nth-child(1) {
- width: 30%;
- }
-
- .manga-table th:nth-child(2),
- .manga-table td:nth-child(2) {
- width: 30%;
- }
-
- .manga-table th:nth-child(3),
- .manga-table td:nth-child(3) {
- width: 30%;
- }
-
- .manga-table th:nth-child(4),
- .manga-table td:nth-child(4) {
- width: 30%;
- }
-
- .manga-table th:nth-child(5),
- .manga-table td:nth-child(5) {
- width: 30%;
- text-align: center;
- }
-
- .manga-table tr:hover {
- background-color: #4c4c4c;
- }
-
- .manga-table a {
- color: #ed2553;
- text-decoration: none;
- font-weight: bold;
- }
-
- .manga-table a:hover {
- text-decoration: underline;
- }
-
- .manga-title {
- display: flex;
- align-items: flex-start;
- flex-wrap: wrap;
- }
-
- .manga-cover {
- width: 50px;
- height: 70px;
- margin-right: 10px;
- margin-bottom: 5px;
- object-fit: cover;
- border-radius: 3px;
- }
-
- .manga-title a {
- display: inline-block;
- /* Allow title to wrap properly */
- word-break: break-word;
- overflow-wrap: break-word;
- width: 100%;
- max-height: 100px;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 4; /* Number of lines to show */
- -webkit-box-orient: vertical;
- }
-
- .progress-bar-container {
- width: 100%;
- background-color: #555;
- height: 8px;
- border-radius: 4px;
- margin-top: 5px;
- }
-
- .progress-bar {
- height: 100%;
- border-radius: 4px;
- background-color: #ed2553;
- }
-
- .language-tag {
- display: inline-block;
- padding: 3px 8px;
- border-radius: 3px;
- background-color: #555;
- font-size: 12px;
- text-transform: capitalize;
- }
-
- .continue-button {
- display: inline-block;
- padding: 6px 12px;
- background-color: #ed2553;
- color: white !important;
- border-radius: 4px;
- text-align: center;
- transition: background-color 0.2s;
- }
-
- .continue-button:hover {
- background-color: #c91c45;
- text-decoration: none !important;
- }
-
- .loading-indicator {
- text-align: center;
- margin: 20px 0;
- font-size: 16px;
- color: #ed2553;
- }
-
- .img-error {
- border: 2px solid #ed2553;
- position: relative;
- }
-
- .remove-button {
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: white;
- color: #ed2553; /* Initial red icon */
- border: 2px solid #ed2553; /* Red border */
- border-radius: 50%;
- width: 30px;
- height: 30px;
- cursor: pointer;
- transition: background-color 0.2s, color 0.2s, transform 0.1s, box-shadow 0.2s;
- font-size: 14px;
- font-weight: bold;
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
- margin: 0 auto;
- }
-
- .remove-button:hover {
- background-color: #ed2553; /* Turns red */
- color: white; /* Icon turns white */
- box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
- transform: scale(1.1);
- }
-
- .remove-button:active {
- transform: scale(0.95);
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
- }
-
- /* Responsive adjustments for smaller screens */
- @media (max-width: 768px) {
- .continue-reading-container {
- width: 98%;
- padding: 10px;
- }
-
- .manga-table th,
- .manga-table td {
- padding: 8px 6px;
- }
-
- .manga-cover {
- width: 40px;
- height: 56px;
- }
-
- .continue-button {
- padding: 4px 8px;
- font-size: 14px;
- }
-
- /* For the continue button column */
- .manga-table td:nth-child(4) {
- position: relative;
- vertical-align: middle;
- }
-
- /* Align the continue button to the bottom */
- .manga-table td:nth-child(4) .continue-button {
- position: relative;
- bottom: 8px;
- left: 6px;
- }
- }
- `;
- document.head.appendChild(customCSS);
-
- // Create container element
- const container = document.createElement('div');
- container.className = 'continue-reading-container';
- document.body.appendChild(container);
-
- // Add title
- const title = document.createElement('h1');
- title.className = 'continue-reading-title';
- title.textContent = 'Continue Reading';
- container.appendChild(title);
-
- // Add loading indicator
- const loadingIndicator = document.createElement('div');
- loadingIndicator.className = 'loading-indicator';
- loadingIndicator.textContent = 'Loading your manga collection...';
- container.appendChild(loadingIndicator);
-
- // Implement the continue reading page
- const mangaList = [];
-
- // Array of possible subdomains to try
- const subdomains = ['t3', 't', 't1', 't2', 't4', 't5', 't7'];
-
- // Helper function to check if an image URL exists
- function checkImageExists(url) {
- return new Promise((resolve, reject) => {
- const img = new Image();
- img.onload = () => resolve(true);
- img.onerror = () => resolve(false);
- img.src = url;
- });
- }
-
- // Helper function to find a working image URL
- async function findWorkingImageUrl(mediaId) {
- for (const subdomain of subdomains) {
- // Try both webp and png formats
- const webpUrl = `https://${subdomain}.nhentai.net/galleries/${mediaId}/cover.webp`;
- const pngUrl = `https://${subdomain}.nhentai.net/galleries/${mediaId}/cover.png`;
- const jpgUrl = `https://${subdomain}.nhentai.net/galleries/${mediaId}/cover.jpg`;
-
- console.log(`Trying cover image URL: ${webpUrl}`);
- const webpExists = await checkImageExists(webpUrl);
- if (webpExists) {
- console.log(`Found working URL: ${webpUrl}`);
- return webpUrl;
- }
-
- console.log(`Trying cover image URL: ${pngUrl}`);
- const pngExists = await checkImageExists(pngUrl);
- if (pngExists) {
- console.log(`Found working URL: ${pngUrl}`);
- return pngUrl;
- }
-
- console.log(`Trying cover image URL: ${jpgUrl}`);
- const jpgExists = await checkImageExists(jpgUrl);
- if (jpgExists) {
- console.log(`Found working URL: ${jpgUrl}`);
- return jpgUrl;
- }
- }
- // If all fail, return the default with t3 subdomain as fallback
- return `https://t3.nhentai.net/galleries/${mediaId}/cover.jpg`;
- }
-
- // Function to create and display the table
- function displayMangaTable() {
- if (mangaList.length === 0) {
- loadingIndicator.textContent = 'No manga found in your collection.';
- return;
- }
-
- // Sort the manga list by most recently read (highest page number to lowest)
- mangaList.sort((a, b) => b.currentPage - a.currentPage);
- console.log('Sorted manga list:', mangaList);
-
- // Create a table to display the manga list
- const table = document.createElement('table');
- table.className = 'manga-table';
- table.innerHTML = `
- <thead>
- <tr>
- <th>Manga Title</th>
- <th>Progress</th>
- <th>Language</th>
- <th>Action</th>
- <th>Remove</th>
- </tr>
- </thead>
- <tbody></tbody>
- `;
-
- const tbody = table.querySelector('tbody');
-
- // Add each manga to the table
- mangaList.forEach(manga => {
- console.log('Adding manga to table:', manga);
- const row = document.createElement('tr');
-
- // Calculate progress percentage
- const progressPercent = (manga.currentPage / manga.pages) * 100;
-
- row.innerHTML = `
- <td>
- <div class="manga-title">
- <img class="manga-cover" src="${manga.coverImageUrl}" alt="Cover" onerror="this.classList.add('img-error')">
- <a href="/g/${manga.id}/" title="${manga.title}">${manga.title}</a>
- </div>
- </td>
- <td>
- <div>Page ${manga.currentPage} of ${manga.pages}</div>
- <div class="progress-bar-container">
- <div class="progress-bar" style="width: ${progressPercent}%"></div>
- </div>
- </td>
- <td><span class="language-tag">${manga.languageDisplay}</span></td>
- <td><a href="/g/${manga.id}/${manga.currentPage}/" class="continue-button" onclick="localStorage.setItem('redirected', 'true');">Continue Reading</a></td>
- <td><button class="remove-button" data-id="${manga.id}">X</button></td>
- `;
- tbody.appendChild(row);
-
- // Remove manga entry from GM storage when 'X' button is clicked
- row.querySelector('.remove-button').addEventListener('click', async function() {
- const mangaId = this.getAttribute('data-id');
- console.log(`Removing manga ID: ${mangaId}`);
-
- // Remove from GM storage
- await GM.deleteValue(mangaId);
- await GM.deleteValue(`metadata_${mangaId}`);
- console.log(`Manga ID ${mangaId} removed from GM storage`);
-
- // Remove row from table
- row.remove();
- });
-
-
- // Remove loading indicator and add the table
- loadingIndicator.remove();
- container.appendChild(table);
- console.log('Table added to page');
-
-
-
- // Handle image loading errors and try alternative subdomains
- const imgElement = row.querySelector('.manga-cover');
- imgElement.addEventListener('error', async function() {
- console.log(`Image failed to load: ${manga.coverImageUrl}`);
-
- // Extract media ID from the URL
- const urlParts = manga.coverImageUrl.split('/');
- const mediaId = urlParts[urlParts.length - 2];
-
- // Find a working URL
- const newUrl = await findWorkingImageUrl(mediaId);
-
- if (newUrl !== manga.coverImageUrl) {
- console.log(`Updating image URL from ${manga.coverImageUrl} to ${newUrl}`);
- this.src = newUrl;
-
- // Update the cached metadata with the working URL
- const metadataKey = `metadata_${manga.id}`;
- GM.getValue(metadataKey, null).then(cachedMetadata => {
- if (cachedMetadata) {
- const metadata = JSON.parse(cachedMetadata);
- metadata.coverImageUrl = newUrl;
- GM.setValue(metadataKey, JSON.stringify(metadata))
- .then(() => console.log(`Updated cached metadata with new URL for manga ID: ${manga.id}`));
- }
- });
- }
- });
- });
-
- // Remove loading indicator and add the table
- loadingIndicator.remove();
- container.appendChild(table);
- console.log('Table added to page');
- }
-
- // Function to fetch manga data from API and save it to GM.setValue
- async function fetchAndSaveMangaData(mangaId, pageNum) {
- const metadataKey = `metadata_${mangaId}`;
-
- // Try to get cached metadata first
- const cachedMetadata = await GM.getValue(metadataKey, null);
-
- if (cachedMetadata) {
- console.log(`Using cached metadata for manga ID: ${mangaId}`);
- const metadata = JSON.parse(cachedMetadata);
-
- mangaList.push({
- id: mangaId,
- title: metadata.title,
- coverImageUrl: metadata.coverImageUrl,
- languageDisplay: metadata.languageDisplay,
- pages: metadata.pages,
- currentPage: pageNum,
- });
-
- // Check if we have all manga data and display the table
- checkAndDisplayTable();
- return;
- }
-
- // If no cached data, fetch from API
- console.log(`Fetching metadata for manga ID: ${mangaId}`);
- try {
- const response = await fetch(`https://nhentai.net/api/gallery/${mangaId}`);
- const data = await response.json();
-
- if (data) {
- console.log('Fetched manga data:', data);
- const mangaTitle = data.title.english;
- const mediaId = data.media_id;
-
- // Get a working cover image URL with appropriate subdomain
- const coverImageUrl = await findWorkingImageUrl(mediaId);
-
- // Determine which language to display
- let languageDisplay = 'Unknown';
- const languages = data.tags.filter(tag => tag.type === 'language').map(tag => tag.name.toLowerCase());
- if (languages.includes('english')) {
- languageDisplay = 'English';
- } else if (languages.includes('translated') && languages.length === 1) {
- languageDisplay = 'English';
- } else if (languages.includes('translated') && languages.length > 1) {
- // Exclude 'translated' and show other language(s)
- const otherLanguages = languages.filter(lang => lang !== 'translated');
- languageDisplay = otherLanguages.length > 0 ? otherLanguages.map(lang => lang.charAt(0).toUpperCase() + lang.slice(1)).join(', ') : 'Unknown';
- } else {
- languageDisplay = languages.map(lang => lang.charAt(0).toUpperCase() + lang.slice(1)).join(', ');
- }
-
- const pages = data.num_pages;
-
- // Create metadata object to cache
- const metadata = {
- title: mangaTitle,
- coverImageUrl: coverImageUrl,
- languageDisplay: languageDisplay,
- pages: pages,
- lastUpdated: Date.now()
- };
-
- // Save metadata to GM storage
- await GM.setValue(metadataKey, JSON.stringify(metadata));
- console.log(`Saved metadata for manga ID: ${mangaId}`);
-
- mangaList.push({
- id: mangaId,
- title: mangaTitle,
- coverImageUrl: coverImageUrl,
- languageDisplay: languageDisplay,
- pages: pages,
- currentPage: pageNum,
- });
-
- // Check if we have all manga data and display the table
- checkAndDisplayTable();
- } else {
- console.log('No data found for manga ID:', mangaId);
- checkAndDisplayTable();
- }
- } catch (error) {
- console.error('Error fetching manga data:', error);
- checkAndDisplayTable();
- }
- }
-
- // Counter to track pending fetch operations
- let pendingFetches = 0;
- let totalMangaCount = 0;
-
- // Function to check if all data is fetched and display the table
- function checkAndDisplayTable() {
- pendingFetches--;
- loadingIndicator.textContent = `Loading your manga collection... (${totalMangaCount - pendingFetches}/${totalMangaCount})`;
-
- if (pendingFetches <= 0) {
- displayMangaTable();
- }
- }
-
-
- // Function to delete completed manga
- async function deleteCompletedManga() {
- const allKeys = await GM.listValues();
- console.log('All keys:', allKeys);
-
- // Get all manga IDs (numerical keys)
- const mangaIds = allKeys.filter(key => key.match(/^\d+$/));
-
- // Process each manga
- for (const mangaId of mangaIds) {
- console.log('Processing manga ID:', mangaId);
- const mangaData = await GM.getValue(mangaId);
- const mangaDataObject = JSON.parse(mangaData);
- const pagesRead = mangaDataObject.pageNum;
- const metadataKey = `metadata_${mangaId}`;
- const metadata = await GM.getValue(metadataKey);
- const metadataObject = JSON.parse(metadata);
- const totalPages = metadataObject.pages;
-
- // Check if manga is completed (one less than or equal to total pages)
- if (pagesRead >= totalPages - 1) {
- console.log(`Deleting completed manga ID: ${mangaId}`);
- await GM.deleteValue(mangaId);
- await GM.deleteValue(metadataKey);
- }
- }
- }
-
-
- // Main function to load manga
- async function getStoredManga() {
- const mangaIds = [];
- const allValues = {};
-
- for (const key of await GM.listValues()) {
- const value = await GM.getValue(key);
- allValues[key] = value;
-
- if (key.match(/^\d+$/)) {
- mangaIds.push(key);
- }
- }
-
- console.log('All values:', allValues);
- totalMangaCount = mangaIds.length;
- pendingFetches = totalMangaCount;
-
- if (totalMangaCount === 0) {
- loadingIndicator.textContent = 'No manga found in your collection.';
- return;
- }
-
- // Process each manga
- mangaIds.forEach(mangaId => {
- console.log('Processing manga ID:', mangaId);
- const mangaData = JSON.parse(allValues[mangaId]);
- fetchAndSaveMangaData(mangaId, mangaData.pageNum);
- });
- }
- deleteCompletedManga();
- // Start the process
- getStoredManga();
- }
-
- //---------------------------**Continue Reading**---------------------------------
-