// ==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**---------------------------------