// ==UserScript==
// @name nhentai Long Strip Mode
// @namespace http://tampermonkey.net/
// @version 1.3.4
// @description Add Long Strip (webtoon) mode to nhentai reader
// @author GrennKren
// @license Unlicense
// @match https://nhentai.net/g/*
// @match https://nhentai.net/g/*/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
let longStripMode = false;
let allImagesLoaded = false;
let imageLoadingMode = 'lazy'; // 'lazy', 'sequential' . Default to lazy, will be overridden by settings
// CDN Configuration
const CDN_CONFIG = {
original_cdns: null,
get primary_cdns() {
return (window._n_app && window._n_app.options && window._n_app.options.image_cdn_urls) || ["i1.nhentai.net", "i2.nhentai.net", "i4.nhentai.net", "i9.nhentai.net"];
},
fallback_cdn: "i.nhentai.net",
selected_cdn: null
};
// Initialize CDN selection (deterministic, consistent across refreshes)
function initializeCDN() {
if (CDN_CONFIG.selected_cdn) return CDN_CONFIG.selected_cdn;
// Save original CDNs
CDN_CONFIG.original_cdns = CDN_CONFIG.primary_cdns.slice();
// Use gallery ID to deterministically select CDN (consistent across refreshes)
const match = window.location.pathname.match(/\/g\/(\d+)/);
const galleryId = match ? match[1] : null;
if (galleryId) {
const availableCDNs = CDN_CONFIG.primary_cdns;
const index = parseInt(galleryId) % availableCDNs.length;
CDN_CONFIG.selected_cdn = availableCDNs[index];
} else {
// Fallback to first CDN if no gallery ID found
CDN_CONFIG.selected_cdn = CDN_CONFIG.primary_cdns[0];
}
console.log(`[nhentai Long Strip] Selected CDN: ${CDN_CONFIG.selected_cdn}`);
console.log(`[nhentai Long Strip] Available CDNs:`, CDN_CONFIG.primary_cdns);
return CDN_CONFIG.selected_cdn;
}
// Get image URL with CDN fallback mechanism
function getImageURLWithFallback(galleryId, page, extension, cdnIndex = 0) {
const cdnList = [...CDN_CONFIG.primary_cdns, CDN_CONFIG.fallback_cdn];
// Always use the available CDNs from the list
if (cdnIndex < cdnList.length) {
return `https://${cdnList[cdnIndex]}/galleries/${galleryId}/${page}.${extension}`;
}
// Final fallback
return `https://${CDN_CONFIG.fallback_cdn}/galleries/${galleryId}/${page}.${extension}`;
}
// Calculate responsive dimensions based on viewport
function calculateResponsiveDimensions(originalWidth, originalHeight, zoomLevel = 100) {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const zoomRatio = zoomLevel / 100;
// Calculate what the image size would be at current zoom
const targetWidth = originalWidth * zoomRatio;
const targetHeight = originalHeight * zoomRatio;
// For 100% zoom (normal), fit to viewport width but don't exceed original size
if (zoomLevel === 100) {
if (originalWidth > viewportWidth) {
// If image is wider than viewport, scale down to fit viewport width
const scale = viewportWidth / originalWidth;
return {
width: viewportWidth,
height: originalHeight * scale,
scale: scale
};
} else {
// If image fits in viewport, use original size
return {
width: originalWidth,
height: originalHeight,
scale: 1
};
}
} else {
// For other zoom levels, apply zoom to the base responsive size
const baseScale = originalWidth > viewportWidth ? viewportWidth / originalWidth : 1;
const finalScale = baseScale * zoomRatio;
return {
width: originalWidth * finalScale,
height: originalHeight * finalScale,
scale: finalScale
};
}
}
// Load image with fallback mechanism
function loadImageWithFallback(galleryId, page, extension, retryCount = 0) {
return new Promise((resolve, reject) => {
const img = new Image();
const maxRetries = CDN_CONFIG.primary_cdns.length + 1; // +1 for final fallback
img.onload = function() {
if (retryCount > 0) {
console.log(`[nhentai Long Strip] Page ${page} loaded successfully with fallback ${retryCount}`);
}
resolve({
success: true,
img: this,
page
});
};
img.onerror = function() {
if (retryCount < maxRetries) {
console.log(`[nhentai Long Strip] Page ${page} failed on attempt ${retryCount + 1}, trying fallback...`);
// Try next CDN
loadImageWithFallback(galleryId, page, extension, retryCount + 1)
.then(result => resolve(result)) // Pass through resolve with result
.catch(error => reject(error));
} else {
console.error(`[nhentai Long Strip] Page ${page} failed to load from all CDNs`);
reject(new Error(`Failed to load page ${page} from all available CDNs`));
}
};
img.src = getImageURLWithFallback(galleryId, page, extension, retryCount);
img.alt = `Page ${page}`;
img.dataset.page = page;
// Set initial styles - let CSS handle the responsiveness
img.style.cssText = `
max-width: 100%;
height: auto;
display: none;
object-fit: contain;
`;
// Use img.decode() if available to ensure full decoding before resolving
if ('decode' in img) {
img.decode()
.then(() => {
resolve({
success: true,
img: img,
page
});
})
.catch((decodeError) => {
// If decode fails, trigger error handling
if (retryCount < maxRetries) {
console.log(`[nhentai Long Strip] Page ${page} decode failed, trying fallback...`);
loadImageWithFallback(galleryId, page, extension, retryCount + 1)
.then(result => resolve(result))
.catch(error => reject(error));
} else {
reject(decodeError);
}
});
}
// If decode not supported, rely on onload (as before)
});
}
// Load images lazily using Intersection Observer
async function loadLazyImages(galleryId, wrappers) {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const page = parseInt(entry.target.dataset.page);
loadImageAsync(galleryId, page, entry.target);
observer.unobserve(entry.target);
}
});
}, { rootMargin: '50px 0px' }); // Trigger when 50px from viewport
wrappers.forEach(wrapper => {
imageObserver.observe(wrapper.imageWrapper);
});
// Load first few images immediately
for (let i = 0; i < Math.min(3, wrappers.length); i++) {
const page = wrappers[i].i;
await loadImageAsync(galleryId, page, wrappers[i].imageWrapper);
}
}
async function loadImageAsync(galleryId, page, imageWrapper) {
try {
const pageData = window.reader.gallery.images.pages[page - 1];
const extension = pageData.extension || 'webp';
const result = await loadImageWithFallback(galleryId, page, extension);
if (result.success) {
const imagePlaceholder = imageWrapper.querySelector('.image-placeholder');
imagePlaceholder.remove();
// Apply current zoom level with responsive behavior
const settings = window.reader.get_settings();
const dimensions = calculateResponsiveDimensions(pageData.width, pageData.height, settings.zoom);
result.img.style.width = dimensions.width + 'px';
result.img.style.height = dimensions.height + 'px';
result.img.style.margin = '0';
result.img.style.padding = '0';
result.img.style.display = 'block';
// Handle horizontal overflow if needed
if (dimensions.width > window.innerWidth || settings.zoom > 100) {
imageWrapper.style.overflowX = 'auto';
result.img.style.maxWidth = 'none';
} else {
imageWrapper.style.overflowX = 'visible';
result.img.style.maxWidth = '100%';
}
imageWrapper.appendChild(result.img);
console.log(`[nhentai Long Strip] Page ${page} loaded lazily`);
}
} catch (error) {
console.error(`[nhentai Long Strip] Page ${page} failed:`, error);
const imagePlaceholder = imageWrapper.querySelector('.image-placeholder');
imagePlaceholder.innerHTML = `
<div style="color: #ff6b6b; text-align: center;">
<div style="font-size: 20px; margin-bottom: 5px;">⚠</div>
<div>Failed to load Page ${page}</div>
<div style="font-size: 12px; margin-top: 5px; cursor: pointer;" onclick="window.location.reload()">
Click to reload page
</div>
</div>
`;
}
}
// Get page position for long strip mode
function getPagePosition(pageNumber) {
const container = document.querySelector('#long-strip-container');
if (!container) return 0;
const imgElement = container.querySelector(`img[data-page="${pageNumber}"]`);
const imageWrapper = imgElement ? imgElement.parentElement : null;
if (!imageWrapper) return 0;
return imageWrapper.offsetTop;
}
// Scroll to specific page in long strip mode
function scrollToPage(pageNumber) {
const position = getPagePosition(pageNumber);
window.scrollTo({ top: position, behavior: 'smooth' });
}
// Apply zoom to all images in long strip mode with responsive behavior
function applyLongStripZoom(zoomLevel) {
const container = document.querySelector('#long-strip-container');
if (!container) return;
const images = container.querySelectorAll('img');
images.forEach(img => {
const pageData = window.reader.gallery.images.pages[parseInt(img.dataset.page) - 1];
if (pageData) {
const dimensions = calculateResponsiveDimensions(pageData.width, pageData.height, zoomLevel);
// Apply calculated dimensions
img.style.width = dimensions.width + 'px';
img.style.height = dimensions.height + 'px';
img.style.maxWidth = 'none'; // Override max-width when zoomed
img.style.margin = '0';
img.style.padding = '0';
// If image exceeds viewport at current zoom, enable scrolling
const wrapper = img.parentElement;
if (dimensions.width > window.innerWidth || zoomLevel > 100) {
wrapper.style.overflowX = 'auto';
wrapper.style.width = '100vw';
} else {
wrapper.style.overflowX = 'visible';
wrapper.style.width = 'auto';
}
}
});
}
// Wait for the page to load and reader to be available
function waitForReader(callback) {
if (window.reader) {
callback();
} else {
setTimeout(() => waitForReader(callback), 100);
}
}
// Add responsive styles
function addResponsiveStyles() {
if (document.getElementById('long-strip-responsive-styles')) return;
const style = document.createElement('style');
style.id = 'long-strip-responsive-styles';
style.textContent = `
#long-strip-container {
width: 100%;
max-width: 100vw;
}
#long-strip-container .image-wrapper {
width: 100%;
max-width: 100vw;
overflow-x: auto;
display: flex;
justify-content: center;
position: relative;
}
#long-strip-container img {
display: block !important;
height: auto;
object-fit: contain;
}
/* Scrollbar styling for horizontal overflow */
#long-strip-container .image-wrapper::-webkit-scrollbar {
height: 8px;
}
#long-strip-container .image-wrapper::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
#long-strip-container .image-wrapper::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
#long-strip-container .image-wrapper::-webkit-scrollbar-thumb:hover {
background: #555;
}
`;
document.head.appendChild(style);
}
// Override the reader's get_settings method to include our new mode
function enhanceReaderSettings() {
if (!window.reader) return;
// Initialize CDN selection
initializeCDN();
// Add responsive styles
addResponsiveStyles();
const originalGetSettings = window.reader.get_settings.bind(window.reader);
const originalSetSettings = window.reader.set_settings.bind(window.reader);
const originalApplySettings = window.reader.apply_settings.bind(window.reader);
const originalPreviousPage = window.reader.previous_page.bind(window.reader);
const originalNextPage = window.reader.next_page.bind(window.reader);
const originalZoomBy = window.reader.zoom_by.bind(window.reader);
// Override get_settings to read long_strip_mode and image_loading_mode from localStorage
window.reader.get_settings = function() {
const settings = originalGetSettings();
settings.long_strip_mode = localStorage.getItem('nhentai_long_strip_mode') === 'true';
settings.image_loading_mode = localStorage.getItem('nhentai_image_loading_mode') || 'sequential';
return settings;
};
// Override previous_page for long strip mode
window.reader.previous_page = function(t) {
if (longStripMode) {
const jumpPages = t || 1;
const targetPage = Math.max(1, this.current_page - jumpPages);
scrollToPage(targetPage);
this.current_page = targetPage;
} else {
originalPreviousPage.call(this, t);
}
};
// Override next_page for long strip mode
window.reader.next_page = function(t) {
if (longStripMode) {
const jumpPages = t || 1;
const targetPage = Math.min(this.gallery.num_pages, this.current_page + jumpPages);
scrollToPage(targetPage);
this.current_page = targetPage;
} else {
originalNextPage.call(this, t);
}
};
// Override zoom_by to support values below 1.0x and handle long strip mode
window.reader.zoom_by = function(t, e, n) {
const settings = this.get_settings();
const minZoom = e || 40; // Changed minimum to 40%
const maxZoom = n || 300;
settings.zoom = Math.max(minZoom, Math.min(settings.zoom + t, maxZoom));
this.set_settings(settings);
if (longStripMode) {
applyLongStripZoom(settings.zoom);
} else {
this.apply_settings();
}
// Update zoom level display
const zoomDisplay = document.querySelector('.zoom-level .value');
if (zoomDisplay) {
zoomDisplay.textContent = (settings.zoom / 100).toFixed(1);
}
};
// Override apply_settings to handle long strip mode and image loading mode
window.reader.apply_settings = function() {
const settings = this.get_settings();
longStripMode = settings.long_strip_mode;
imageLoadingMode = settings.image_loading_mode;
if (longStripMode) {
this.enableLongStripMode();
} else {
this.disableLongStripMode();
}
originalApplySettings.call(this);
// Update zoom display
const zoomDisplay = document.querySelector('.zoom-level .value');
if (zoomDisplay) {
zoomDisplay.textContent = (settings.zoom / 100).toFixed(1);
}
};
// Sequential load images and attach to containers
async function loadSequentialImages(galleryId, totalImages, wrappers) {
for (let i = 1; i <= totalImages; i++) {
try {
const pageData = window.reader.gallery.images.pages[i - 1];
const extension = pageData.extension || 'webp';
const wrapper = wrappers[i - 1];
const result = await loadImageWithFallback(galleryId, i, extension);
if (result.success) {
const imageWrapper = wrapper.imageWrapper;
const imagePlaceholder = imageWrapper.querySelector('.image-placeholder');
imagePlaceholder.remove();
// Apply current zoom level with responsive behavior
const settings = window.reader.get_settings();
const dimensions = calculateResponsiveDimensions(pageData.width, pageData.height, settings.zoom);
result.img.style.width = dimensions.width + 'px';
result.img.style.height = dimensions.height + 'px';
result.img.style.margin = '0';
result.img.style.padding = '0';
result.img.style.display = 'block';
// Handle horizontal overflow if needed
if (dimensions.width > window.innerWidth || settings.zoom > 100) {
imageWrapper.style.overflowX = 'auto';
result.img.style.maxWidth = 'none';
} else {
imageWrapper.style.overflowX = 'visible';
result.img.style.maxWidth = '100%';
}
imageWrapper.appendChild(result.img);
console.log(`[nhentai Long Strip] Page ${i} loaded sequentially`);
}
} catch (error) {
console.error(`[nhentai Long Strip] Page ${i} failed:`, error);
const wrapper = wrappers[i - 1];
const imageWrapper = wrapper.imageWrapper;
const imagePlaceholder = imageWrapper.querySelector('.image-placeholder');
imagePlaceholder.innerHTML = `
<div style="color: #ff6b6b; text-align: center;">
<div style="font-size: 20px; margin-bottom: 5px;">⚠</div>
<div>Failed to load Page ${i}</div>
<div style="font-size: 12px; margin-top: 5px; cursor: pointer;" onclick="window.location.reload()">
Click to reload page
</div>
</div>
`;
}
}
}
// Add long strip mode functionality
window.reader.enableLongStripMode = function() {
if (allImagesLoaded) return;
// Override CDNs to use only the selected one to prevent inconsistencies with site's random selection
//window._n_app.options.image_cdn_urls = [CDN_CONFIG.selected_cdn];
// Disable preloading to avoid concurrent loading
const settings = this.get_settings();
const originalPreload = settings.preload;
settings.preload = 0; // Disable preloading
this.set_settings(settings);
const imageContainer = document.querySelector('#image-container');
const gallery = this.gallery;
// Hide pagination controls in long strip mode
document.querySelectorAll('.reader-pagination').forEach(el => {
el.style.display = 'none';
});
// Create container for all images
const longStripContainer = document.createElement('div');
longStripContainer.id = 'long-strip-container';
longStripContainer.style.cssText = `
display: flex;
flex-direction: column;
align-items: center;
gap: 0px;
width: 100%;
max-width: 100vw;
`;
// Add CSS animation for spinner if not present
if (!document.getElementById('spinner-style')) {
const style = document.createElement('style');
style.id = 'spinner-style';
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
}
const totalImages = gallery.num_pages;
const galleryId = gallery.media_id;
const wrappers = [];
// Create all wrappers first (synchronously)
for (let i = 1; i <= gallery.num_pages; i++) {
// Create image wrapper with placeholder
const imageWrapper = document.createElement('div');
imageWrapper.style.cssText = `
position: relative;
width: 100%;
max-width: 100vw;
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
background: #f0f0f0;
margin: 0;
padding: 0;
overflow-x: auto;
`;
imageWrapper.dataset.page = i;
// Create placeholder spinner for each image
const imagePlaceholder = document.createElement('div');
imagePlaceholder.className = 'image-placeholder'; // Add class for selector
imagePlaceholder.style.cssText = `
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #666;
`;
const imageSpinner = document.createElement('div');
imageSpinner.style.cssText = `
width: 30px;
height: 30px;
border: 3px solid #ddd;
border-top: 3px solid #666;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
`;
const pageText = document.createElement('div');
pageText.textContent = `Page ${i}`;
pageText.style.fontSize = '14px';
imagePlaceholder.appendChild(imageSpinner);
imagePlaceholder.appendChild(pageText);
imageWrapper.appendChild(imagePlaceholder);
imageWrapper.classList.add('image-wrapper');
longStripContainer.appendChild(imageWrapper);
wrappers.push({ imageWrapper, i });
}
// Replace current image with long strip container
imageContainer.innerHTML = '';
imageContainer.appendChild(longStripContainer);
// Choose loading method based on setting
if (imageLoadingMode === 'lazy') {
loadLazyImages(galleryId, wrappers);
} else {
loadSequentialImages(galleryId, totalImages, wrappers);
}
allImagesLoaded = true;
// Add window resize handler for responsive behavior
window.addEventListener('resize', () => {
if (longStripMode) {
const currentSettings = this.get_settings();
setTimeout(() => applyLongStripZoom(currentSettings.zoom), 100);
}
});
};
window.reader.disableLongStripMode = function() {
if (!allImagesLoaded) {
// If we are already in normal mode, ensure pagination is visible
document.querySelectorAll('.reader-pagination').forEach(el => {
el.style.display = '';
});
return;
}
// Restore original CDNs
// window._n_app.options.image_cdn_urls = CDN_CONFIG.original_cdns;
// Restore original preload setting
const settings = this.get_settings();
const originalPreload = settings.preload;
if (this.originalPreload !== undefined) {
settings.preload = this.originalPreload;
this.set_settings(settings);
}
const imageContainer = document.querySelector('#image-container');
// Show pagination controls
document.querySelectorAll('.reader-pagination').forEach(el => {
el.style.display = '';
});
// Restore original image
const currentPageImage = this.get_image(this.current_page);
imageContainer.innerHTML = '';
const link = document.createElement('a');
link.href = this.get_page_url(this.current_page + 1);
link.appendChild(currentPageImage.image);
imageContainer.appendChild(link);
allImagesLoaded = false;
};
// Helper method to get image URL (updated to use CDN system)
window.reader.get_image_url = function(page, pageData) {
const mediaId = this.gallery.media_id;
const extension = pageData.extension || 'webp';
return getImageURLWithFallback(mediaId, page, extension, 0);
};
}
// Modify the settings modal to include long strip option
function addLongStripToSettings() {
const checkForSettings = setInterval(() => {
const settingsButton = document.querySelector('.reader-settings');
if (settingsButton) {
clearInterval(checkForSettings);
const newButton = settingsButton.cloneNode(true);
settingsButton.parentNode.replaceChild(newButton, settingsButton);
newButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const existingModals = document.querySelectorAll('.modal-wrapper');
existingModals.forEach(modal => modal.remove());
showEnhancedSettingsModal();
});
}
}, 100);
// Also override zoom buttons to work with our extended range
const checkForZoomButtons = setInterval(() => {
const zoomInBtn = document.querySelector('.reader-zoom-in');
const zoomOutBtn = document.querySelector('.reader-zoom-out');
if (zoomInBtn && zoomOutBtn) {
clearInterval(checkForZoomButtons);
// Clone and replace zoom in button
const newZoomInBtn = zoomInBtn.cloneNode(true);
zoomInBtn.parentNode.replaceChild(newZoomInBtn, zoomInBtn);
newZoomInBtn.addEventListener('click', function() {
window.reader.zoom_by(20, 40, 300);
});
// Clone and replace zoom out button
const newZoomOutBtn = zoomOutBtn.cloneNode(true);
zoomOutBtn.parentNode.replaceChild(newZoomOutBtn, zoomOutBtn);
newZoomOutBtn.addEventListener('click', function() {
window.reader.zoom_by(-20, 40, 300);
});
}
}, 100);
}
function showEnhancedSettingsModal() {
if (!window.reader) return;
const settings = window.reader.get_settings();
const modalHTML = `
<div class="modal-wrapper fade-slide-in open" id="enhanced-settings-modal">
<div class="modal-inner">
<h1>Reader Settings</h1>
<div class="contents">
<div id="reader-settings">
<label>Reading Mode
<select class="control" data-setting="long_strip_mode">
<option value="false">Normal (Page by Page)</option>
<option value="true">Long Strip (Webtoon)</option>
</select>
</label>
<label>Image Loading Mode (Long Strip Only)
<select class="control" data-setting="image_loading_mode">
<option value="sequential">Sequential (Load all at once)</option>
<option value="lazy">Lazy Loading (Load on scroll)</option>
</select>
</label>
<label>Image Preloading
<select class="control" data-setting="preload">
<option value="0">Disabled</option>
<option value="1">1 page</option>
<option value="2">2 pages</option>
<option value="3">3 pages</option>
<option value="4">4 pages</option>
<option value="5">5 pages</option>
</select>
</label>
<label>Page Turning
<select class="control" data-setting="turning_behavior">
<option value="left">Left half</option>
<option value="right">Right half</option>
<option value="both">Entire image</option>
</select>
</label>
<label>Image Scaling
<select class="control" data-setting="image_scaling">
<option value="fit-horizontal">Fit horizontally</option>
<option value="fit-both">Fit on screen</option>
</select>
</label>
<label>Zoom
<select class="control" data-setting="zoom">
<option value="40">40%</option>
<option value="60">60%</option>
<option value="80">80%</option>
<option value="100">100% (Viewport Fit)</option>
<option value="120">120%</option>
<option value="140">140%</option>
<option value="160">160%</option>
<option value="180">180%</option>
<option value="200">200%</option>
<option value="220">220%</option>
<option value="240">240%</option>
<option value="260">260%</option>
<option value="280">280%</option>
<option value="300">300%</option>
</select>
</label>
<label>Jump on Page Turn
<select class="control" data-setting="jump_on_turn">
<option value="image">Top of image</option>
<option value="controls">Top of controls</option>
<option value="none">Don't jump</option>
</select>
</label>
<label>Scroll Speed
<input class="control" type="range" min="1" max="20" step="1" data-setting="scroll_speed" />
</label>
<p>
<h2>Keyboard Shortcuts</h2>
<table>
<tr>
<td><code>W</code>, <code>↑</code></td>
<td>Scroll up</td>
</tr>
<tr>
<td><code>A</code>, <code>←</code></td>
<td>Back one page (Long Strip: scroll to previous page)</td>
</tr>
<tr>
<td><code>S</code>, <code>↓</code></td>
<td>Scroll down</td>
</tr>
<tr>
<td><code>D</code>, <code>→</code></td>
<td>Forward one page (Long Strip: scroll to next page)</td>
</tr>
<tr>
<td><code>F</code></td>
<td>Change image scaling mode</td>
</tr>
<tr>
<td><code>Shift</code></td>
<td>Skip 5 pages faster (Long Strip: jump 5 pages)</td>
</tr>
<tr>
<td><code>+</code>, <code>-</code></td>
<td>Zoom (Long Strip: affects all images, responsive)</td>
</tr>
</table>
</p>
</div>
</div>
<div class="buttons">
<button type="button" class="btn btn-primary" id="save-settings">Save</button>
<button type="button" class="btn btn-secondary" id="cancel-settings">Cancel</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
const modal = document.getElementById('enhanced-settings-modal');
// Set current values
Object.keys(settings).forEach(key => {
if (key !== 'version') {
const control = modal.querySelector(`[data-setting="${key}"]`);
if (control) {
control.value = settings[key];
}
}
});
// Handle save button
modal.querySelector('#save-settings').addEventListener('click', function() {
// Read values from modal before removing it
const longStripModeValue = modal.querySelector('[data-setting="long_strip_mode"]').value;
localStorage.setItem('nhentai_long_strip_mode', longStripModeValue);
const imageLoadingModeValue = modal.querySelector('[data-setting="image_loading_mode"]').value;
localStorage.setItem('nhentai_image_loading_mode', imageLoadingModeValue);
const originalSettings = window.reader.get_settings();
delete originalSettings.long_strip_mode;
delete originalSettings.image_loading_mode;
modal.querySelectorAll('.control').forEach(control => {
const setting = control.dataset.setting;
if (setting !== 'long_strip_mode' && setting !== 'image_loading_mode') {
let value = control.value;
if (typeof originalSettings[setting] === 'number') {
value = parseInt(value, 10);
}
originalSettings[setting] = value;
}
});
// Now remove the modal
modal.remove();
// Then apply settings
window.reader.set_settings(originalSettings);
window.reader.apply_settings();
});
// Handle cancel button
modal.querySelector('#cancel-settings').addEventListener('click', function() {
modal.remove();
});
// Handle click outside modal
modal.addEventListener('click', function(e) {
if (e.target === modal) {
modal.remove();
}
});
}
// Initialize when page loads
function initialize() {
// Check if we're on a reader page
if (window.location.pathname.match(/\/g\/\d+\/\d+\//)) {
waitForReader(() => {
enhanceReaderSettings();
addLongStripToSettings();
window.reader.apply_settings();
});
}
}
// Run initialization
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();