With the goal of being light, discreet, and non-invasive, a QoL script for Exhentai and E-hentai. It adds features like horizontal and vertical reading, infinite scrolling, pre-fetching images, and more to make browsing extremely smooth.
Stan na
// ==UserScript==
// @name Ex Fast Reader
// @namespace https://tampermonkey.net/
// @version 4.4.2
// @description With the goal of being light, discreet, and non-invasive, a QoL script for Exhentai and E-hentai. It adds features like horizontal and vertical reading, infinite scrolling, pre-fetching images, and more to make browsing extremely smooth.
// @author ethyr
// @match *://e-hentai.org/*
// @match *://exhentai.org/*
// @match *://*.e-hentai.org/*
// @match *://*.exhentai.org/*
// @icon https://e-hentai.org/favicon.ico
// @exclude *://forums.e-hentai.org/*
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
// =========================================================================
// 0. CORE & SEAMLESS STRIP STYLES
// =========================================================================
const CORE_CSS = `
/* Failure Toast */
.eh-toast-fail {
position: fixed;
bottom: 24px;
right: 24px;
background: rgba(26, 18, 18, 0.96);
color: #fca5a5;
border: 1px solid #ef4444;
padding: 9px 16px;
border-radius: 6px;
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
box-shadow: 0 4px 14px rgba(0,0,0,0.5);
z-index: 999999;
cursor: pointer;
}
/* Seamless Native-Themed Controls */
.eh-strip-controls {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 3px 8px;
vertical-align: middle;
}
.eh-strip-controls.single-mode-bottom {
display: flex !important;
justify-content: center !important;
align-items: center !important;
margin: 1px auto 2px auto !important;
padding: 0 !important;
text-align: center !important;
clear: both !important;
width: 100% !important;
}
/* In single-page mode, strictly hide every control except the strip mode toggle button */
.eh-strip-controls.single-mode-bottom > :not(#eh-strip-mode-toggle) {
display: none !important;
}
.eh-strip-btn {
display: inline-flex;
align-items: center !important;
justify-content: center !important;
font-family: arial, helvetica, sans-serif !important;
font-size: 9pt !important;
font-weight: bold !important;
min-height: 26px !important;
line-height: 20px !important;
padding: 1px 12px 2px !important;
margin: 0 2px !important;
border-radius: 3px !important;
cursor: pointer !important;
text-decoration: none !important;
user-select: none !important;
white-space: nowrap !important;
box-sizing: border-box !important;
-webkit-text-stroke: none !important;
text-shadow: none !important;
outline: none !important;
vertical-align: middle !important;
transition: background-color 0.1s ease, border-color 0.1s ease !important;
border: 2px solid #B5A4A4 !important;
background-color: #EDEADA !important;
color: #5C0D12 !important;
}
.eh-strip-btn b,
.eh-strip-btn span {
font-weight: normal;
-webkit-text-stroke: none !important;
text-shadow: none !important;
}
.eh-strip-btn.active b {
font-weight: bold;
}
.eh-strip-page-counter {
font-family: arial, helvetica, sans-serif;
font-size: 9pt;
font-weight: normal;
padding: 2px 6px;
user-select: none;
white-space: nowrap;
line-height: normal;
}
/* E-Hentai Theme (Light) - Exact Match to Native Site Buttons */
.eh-theme-light .eh-strip-btn,
.eh-strip-btn {
background-color: #EDEADA !important;
color: #5C0D12 !important;
border: 2px solid #B5A4A4 !important;
border-radius: 3px !important;
-webkit-text-stroke: none !important;
text-shadow: none !important;
}
.eh-theme-light .eh-strip-btn:hover {
background-color: #F3F0E0 !important;
border-color: #977273 !important;
color: #5C0D12 !important;
}
.eh-theme-light .eh-strip-btn.active {
background-color: #D7D3C2 !important;
border-color: #5C0D12 !important;
color: #5C0D12 !important;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.25) !important;
}
.eh-theme-light .eh-strip-page-counter,
.eh-theme-light #eh-topbar-info {
color: #5C0D12 !important;
paint-order: stroke fill !important;
-webkit-text-stroke: 0.6px #ffffff !important;
text-shadow: none !important;
}
/* ExHentai Theme (Dark) - Exact Match to Native Site Buttons */
.eh-theme-dark .eh-strip-btn {
background-color: #34353b !important;
color: #dddddd !important;
border: 2px solid #8d8d8d !important;
border-radius: 3px !important;
-webkit-text-stroke: none !important;
text-shadow: none !important;
}
.eh-theme-dark .eh-strip-btn:hover {
background-color: #43464e !important;
border-color: #aeaeae !important;
color: #ffffff !important;
}
.eh-theme-dark .eh-strip-btn.active {
background-color: #1a1a1a !important;
border-color: #aeaeae !important;
color: #ffffff !important;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4) !important;
}
.eh-theme-dark .eh-strip-page-counter,
.eh-theme-dark #eh-topbar-info {
color: #dddddd !important;
paint-order: stroke fill !important;
-webkit-text-stroke: 0.6px #000000 !important;
text-shadow: none !important;
}
/* Hide title bar (h1, #nb, #gn), original switchers, bottom bars, footers, and redundant links cleanly via CSS */
html.eh-strip-mode-active h1,
html.eh-strip-mode-active #nb,
html.eh-strip-mode-active #gn,
html.eh-strip-mode-active #gj,
html.eh-strip-mode-active .sb,
html.eh-strip-mode-active .sn,
html.eh-strip-mode-active #i3,
html.eh-strip-mode-active #i4,
html.eh-strip-mode-active #i5,
html.eh-strip-mode-active #i6,
html.eh-strip-mode-active #i7,
html.eh-strip-mode-active .c,
html.eh-strip-mode-active .c1,
html.eh-strip-mode-active .c2,
html.eh-strip-mode-active .c3,
html.eh-strip-mode-active #footer,
html.eh-strip-mode-active .dp,
html.eh-strip-mode-active #dp,
html.eh-strip-mode-active .ip,
html.eh-strip-mode-active #ip,
html.eh-strip-mode-active .ft,
html.eh-strip-mode-active #ft,
html.eh-strip-mode-active .gpc,
html.eh-strip-mode-active #gpc,
html.eh-strip-mode-active div.d,
html.eh-strip-mode-active #i2 > div:not(.sn):not(#eh-strip-controls):not(#eh-topbar-info),
html.eh-strip-mode-active #i2 > p,
html.eh-strip-mode-active #i2 > span:not(.sn),
html.eh-strip-mode-active #i2 > a:not(#eh-strip-controls a),
html.eh-strip-mode-active .eh-strip-hidden-bottom,
body.eh-strip-mode-active h1,
body.eh-strip-mode-active #nb,
body.eh-strip-mode-active #gn,
body.eh-strip-mode-active #gj,
body.eh-strip-mode-active .sb,
body.eh-strip-mode-active .sn,
body.eh-strip-mode-active #i3,
body.eh-strip-mode-active #i4,
body.eh-strip-mode-active #i5,
body.eh-strip-mode-active #i6,
body.eh-strip-mode-active #i7,
body.eh-strip-mode-active .c,
body.eh-strip-mode-active .c1,
body.eh-strip-mode-active .c2,
body.eh-strip-mode-active .c3,
body.eh-strip-mode-active #footer,
body.eh-strip-mode-active .dp,
body.eh-strip-mode-active #dp,
body.eh-strip-mode-active .ip,
body.eh-strip-mode-active #ip,
body.eh-strip-mode-active .ft,
body.eh-strip-mode-active #ft,
body.eh-strip-mode-active .gpc,
body.eh-strip-mode-active #gpc,
body.eh-strip-mode-active div.d,
body.eh-strip-mode-active #i2 > div:not(.sn):not(#eh-strip-controls):not(#eh-topbar-info),
body.eh-strip-mode-active #i2 > p,
body.eh-strip-mode-active #i2 > span:not(.sn),
body.eh-strip-mode-active #i2 > a:not(#eh-strip-controls a),
body.eh-strip-mode-active .eh-strip-hidden-bottom {
display: none !important;
}
/* Hide strip topbar info when strip mode is inactive */
body:not(.eh-strip-mode-active) #eh-topbar-info {
display: none !important;
}
/* TOP BAR: Fully extends to both edges; auto-hides unless hovered or focused */
html.eh-strip-mode-active #i2,
body.eh-strip-mode-active #i2 {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
width: 100vw !important;
width: 100% !important;
max-width: 100vw !important;
margin: 0 !important;
padding: 6px 12px !important;
box-sizing: border-box !important;
z-index: 100000 !important;
display: flex !important;
justify-content: center !important;
align-items: center !important;
transform: translateY(-100%) !important;
opacity: 0 !important;
visibility: hidden !important;
box-shadow: none !important;
border: none !important;
pointer-events: none !important;
transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.22s ease, visibility 0.22s !important;
}
body.eh-theme-light #i2 {
background-color: #edeada !important;
}
body.eh-theme-dark #i2 {
background-color: rgba(52, 53, 59, 0.97) !important;
backdrop-filter: blur(8px) !important;
}
body.eh-theme-light.eh-topbar-visible #i2 {
border-bottom: 1px solid #5c0d12 !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2) !important;
}
body.eh-theme-dark.eh-topbar-visible #i2 {
border-bottom: 1px solid #202020 !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4) !important;
}
/* High contrast solid stroke on top-bar text without fade/glow */
body.eh-theme-light.eh-strip-mode-active #i2,
body.eh-theme-light.eh-strip-mode-active #i2 a:not(.eh-strip-btn),
body.eh-theme-light.eh-strip-mode-active #i2 span:not(.eh-strip-btn):not(#eh-strip-controls *),
body.eh-theme-light.eh-strip-mode-active #i2 b:not(#eh-strip-controls *) {
color: #5c0d12;
paint-order: stroke fill;
-webkit-text-stroke: 0.6px #ffffff;
text-shadow: none !important;
}
body.eh-theme-dark.eh-strip-mode-active #i2,
body.eh-theme-dark.eh-strip-mode-active #i2 a:not(.eh-strip-btn),
body.eh-theme-dark.eh-strip-mode-active #i2 span:not(.eh-strip-btn):not(#eh-strip-controls *),
body.eh-theme-dark.eh-strip-mode-active #i2 b:not(#eh-strip-controls *) {
color: #dddddd;
paint-order: stroke fill;
-webkit-text-stroke: 0.6px #000000;
text-shadow: none !important;
}
body.eh-strip-mode-active.eh-topbar-visible #i2 {
transform: translateY(0) !important;
opacity: 1 !important;
visibility: visible !important;
pointer-events: auto !important;
}
/* Container constraints in horizontal mode: maximize vertical space */
html.eh-strip-mode-active.strip-horizontal,
body.eh-strip-mode-active.strip-horizontal {
overflow: hidden !important;
height: 100vh !important;
max-height: 100vh !important;
margin: 0 !important;
padding: 0 !important;
}
html.eh-strip-mode-active.strip-horizontal #i1,
body.eh-strip-mode-active.strip-horizontal #i1 {
width: 100% !important;
max-width: 100vw !important;
height: 100vh !important;
max-height: 100vh !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
outline: none !important;
box-sizing: border-box !important;
overflow: hidden !important;
position: relative !important;
}
/* Vertical mode: Clean layout with no excessive bottom padding */
html.eh-strip-mode-active.strip-vertical #i1,
body.eh-strip-mode-active.strip-vertical #i1 {
padding-bottom: 0 !important;
margin-bottom: 0 !important;
}
/* COMPLETELY SEAMLESS RIGHT-TO-LEFT (RTL) HORIZONTAL STRIP */
#eh-strip-container.layout-horizontal {
display: flex;
flex-direction: row;
direction: rtl; /* RIGHT TO LEFT (Manga standard) */
align-items: center;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
height: 100vh !important;
max-height: 100vh !important;
width: 100vw;
box-sizing: border-box;
padding: 0;
margin: 0;
gap: 0;
border: none;
scroll-behavior: auto;
overscroll-behavior-x: contain;
overflow-anchor: none !important;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
scrollbar-color: #4f535b rgba(0, 0, 0, 0.22);
}
.eh-theme-light #eh-strip-container.layout-horizontal {
scrollbar-color: #5c0d12 rgba(0, 0, 0, 0.22);
}
/* Items are sized via aspect-ratio and width calculated to match rendered 100vh height */
#eh-strip-container.layout-horizontal .eh-strip-item {
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex: 0 0 auto;
height: 100vh !important;
max-height: 100vh !important;
width: calc(100vh * var(--eh-ar, 0.707));
aspect-ratio: var(--eh-ar, 0.707);
margin: 0;
padding: 0;
border: none;
position: relative;
box-sizing: border-box;
overflow: hidden;
overflow-anchor: none !important;
}
#eh-strip-container.layout-horizontal .eh-strip-item img.eh-main-img {
height: 100vh !important;
max-height: 100vh !important;
width: auto;
max-width: none;
object-fit: contain;
display: block;
margin: 0;
padding: 0;
border: none;
box-shadow: none;
vertical-align: middle;
position: relative;
z-index: 2;
}
/* End Card at the end of horizontal strip: generous leeway allows the last image to be centered */
.eh-strip-item.eh-strip-end-card {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
height: 100vh !important;
width: auto !important;
min-width: max(200px, calc(50vw - (100vh * var(--eh-ar, 0.707)) / 2 + 60px)) !important;
aspect-ratio: auto !important;
padding: 0 40px !important;
flex: 0 0 auto;
z-index: 3;
box-sizing: border-box !important;
}
/* Start Spacer for Horizontal RTL mode: provides generous leeway on the right so Page 1 can be centered */
.eh-strip-start-spacer {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
height: 100vh !important;
max-height: 100vh !important;
width: max(40px, calc(50vw - (100vh * var(--eh-ar, 0.707)) / 2)) !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
box-sizing: border-box !important;
flex: 0 0 auto !important;
pointer-events: none !important;
overflow-anchor: none !important;
background: transparent !important;
}
#eh-strip-container.layout-vertical .eh-strip-start-spacer {
display: none !important;
}
/* COMPLETELY SEAMLESS VERTICAL STRIP */
#eh-strip-container.layout-vertical {
display: flex;
flex-direction: column;
align-items: center;
overflow-y: visible;
overflow-x: hidden;
width: 100%;
margin: 0 auto;
padding: 0;
gap: 0;
border: none;
overflow-anchor: none !important;
}
#eh-strip-container.layout-vertical .eh-strip-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
max-width: var(--eh-max-w, 100%);
aspect-ratio: var(--eh-ar, 0.707);
margin: 0 auto;
padding: 0;
border: none;
position: relative;
box-sizing: border-box;
overflow: hidden;
overflow-anchor: none !important;
}
#eh-strip-container.layout-vertical .eh-strip-item img.eh-main-img {
max-width: 100%;
width: 100%;
height: auto;
aspect-ratio: var(--eh-ar, 0.707);
display: block;
margin: 0 auto;
padding: 0;
border: none;
box-shadow: none;
position: relative;
z-index: 2;
}
/* Top Bar Image Info */
#eh-topbar-info {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-family: arial, helvetica, sans-serif;
font-size: 9pt;
font-weight: normal;
margin: 0 10px;
white-space: nowrap;
user-select: none;
-webkit-text-stroke: none !important;
text-shadow: none !important;
}
#eh-topbar-info a {
text-decoration: underline;
}
/* Stretched Thumbnail Placeholders (Unzoomed 1:1) */
.eh-placeholder-thumb-wrap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
z-index: 1;
transform: none;
}
.eh-placeholder-thumb-img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: fill;
filter: blur(2px);
opacity: 1;
pointer-events: none;
z-index: 1;
transform: none;
}
.eh-placeholder-thumb-sprite {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-repeat: no-repeat;
filter: blur(2px);
opacity: 1;
transform: none;
pointer-events: none;
}
/* Clean, Theme-Consistent Solid Neutral Placeholder */
.eh-strip-item.eh-placeholder {
box-sizing: border-box;
position: relative;
}
/* Only show & animate the spinner when the page is actively loading */
.eh-placeholder:not(.eh-loading) .eh-spinner {
display: none !important;
}
.eh-theme-light .eh-strip-item.eh-placeholder {
background-color: #e8e4d6;
outline: 1px dashed rgba(92, 13, 18, 0.22);
outline-offset: -1px;
border: none;
}
.eh-theme-dark .eh-strip-item.eh-placeholder {
background-color: #27282c;
outline: 1px dashed rgba(255, 255, 255, 0.12);
outline-offset: -1px;
border: none;
}
/* Discreet, elegant frosted loading capsule */
.eh-placeholder-content {
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 7px;
user-select: none;
position: relative;
z-index: 2;
background: rgba(0, 0, 0, 0.42);
padding: 5px 12px;
border-radius: 14px;
backdrop-filter: blur(3px);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
pointer-events: none;
}
.eh-placeholder-content.eh-placeholder-retry {
flex-direction: column;
gap: 8px;
padding: 12px 18px;
border-radius: 8px;
pointer-events: auto;
}
.eh-spinner {
width: 14px;
height: 14px;
border: 2px solid rgba(255, 255, 255, 0.22);
border-top-color: #ffffff;
border-radius: 50%;
animation: eh-spin 0.7s linear infinite;
flex-shrink: 0;
}
.eh-theme-light .eh-spinner {
border-top-color: #5c0d12;
border-color: rgba(92, 13, 18, 0.2);
}
.eh-theme-dark .eh-spinner {
border-top-color: #ffffff;
border-color: rgba(255, 255, 255, 0.22);
}
@keyframes eh-spin {
to { transform: rotate(360deg); }
}
.eh-placeholder-text {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, arial, sans-serif;
font-size: 8.5pt;
font-weight: 600;
color: #ffffff;
opacity: 0.9;
letter-spacing: 0.3px;
line-height: 1;
white-space: nowrap;
}
.eh-theme-light .eh-placeholder-text {
color: #5c0d12;
}
.eh-theme-dark .eh-placeholder-text {
color: #eeeeee;
}
/* Scrollbar styling with comfortable leeway for horizontal sliding */
#eh-strip-container.layout-horizontal::-webkit-scrollbar {
height: 12px;
}
#eh-strip-container.layout-horizontal::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.22);
}
.eh-theme-light #eh-strip-container.layout-horizontal::-webkit-scrollbar-thumb {
background: #5c0d12;
border-radius: 6px;
}
.eh-theme-light #eh-strip-container.layout-horizontal::-webkit-scrollbar-thumb:hover {
background: #87141b;
}
.eh-theme-dark #eh-strip-container.layout-horizontal::-webkit-scrollbar-thumb {
background: #4f535b;
border-radius: 6px;
}
.eh-theme-dark #eh-strip-container.layout-horizontal::-webkit-scrollbar-thumb:hover {
background: #6a6f7b;
}
/* Native-Themed Single Bottom Return to Gallery Button (Compact, No Extra Space) */
.eh-bottom-gallery-bar {
display: flex !important;
justify-content: center !important;
align-items: center !important;
margin: 15px auto 18px auto !important;
padding: 6px 0 !important;
text-align: center !important;
clear: both !important;
width: 100% !important;
}
.eh-bottom-gallery-bar a {
display: inline-block;
padding: 6px 20px;
font-family: arial, helvetica, sans-serif;
font-size: 10pt;
font-weight: bold;
text-decoration: none;
border-radius: 3px;
cursor: pointer;
transition: all 0.15s ease-in-out;
}
.eh-theme-light .eh-bottom-gallery-bar a {
background-color: #edeada;
color: #5c0d12;
border: 1px solid #5c0d12;
}
.eh-theme-light .eh-bottom-gallery-bar a:hover {
background-color: #f2efdf;
color: #9b4e03;
border-color: #9b4e03;
}
.eh-theme-dark .eh-bottom-gallery-bar a {
background-color: #34353b;
color: #dddddd;
border: 1px solid #4f535b;
}
.eh-theme-dark .eh-bottom-gallery-bar a:hover {
background-color: #43464e;
color: #ffffff;
border-color: #727783;
}
/* Infinite Scroll Button */
#eh-infinite-scroll-wrap {
display: inline-flex !important;
align-items: center !important;
margin-left: 8px !important;
vertical-align: middle !important;
}
#eh-infinite-scroll-toggle {
font-family: inherit;
font-size: 9pt;
line-height: normal;
box-sizing: border-box;
vertical-align: middle;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 10px;
min-height: 22px !important;
height: 22px;
}
/* Light Theme: keep the hover background (#F3F0E0) and border (#977273) when active */
.eh-theme-light #eh-infinite-scroll-toggle.active {
background-color: #F3F0E0 !important;
border-color: #977273 !important;
color: #5C0D12 !important;
box-shadow: none !important;
}
/* Dark Theme: keep the hover background (#43464e) and border (#aeaeae) when active */
.eh-theme-dark #eh-infinite-scroll-toggle.active {
background-color: #43464e !important;
border-color: #aeaeae !important;
color: #ffffff !important;
box-shadow: none !important;
}
/* Single page thumbnail placeholder */
#i3 {
position: relative !important;
text-align: center !important;
}
#i3 a {
position: relative !important;
display: inline-block !important;
max-width: 100% !important;
}
#eh-single-thumb-placeholder {
position: relative !important;
margin: 0 auto !important;
overflow: hidden !important;
pointer-events: none !important;
z-index: 1 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
transition: opacity 0.15s ease-out !important;
box-sizing: border-box !important;
}
.eh-theme-light #eh-single-thumb-placeholder {
background-color: #e8e4d6;
}
.eh-theme-dark #eh-single-thumb-placeholder {
background-color: #27282c;
}
#eh-single-thumb-placeholder .eh-placeholder-thumb-wrap {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
overflow: hidden !important;
pointer-events: none !important;
z-index: 1 !important;
}
#eh-single-thumb-placeholder .eh-placeholder-thumb-img {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
object-fit: fill !important;
filter: blur(2px) !important;
opacity: 1 !important;
pointer-events: none !important;
}
#i3 img#img {
position: relative !important;
z-index: 2 !important;
display: block !important;
margin: 0 auto !important;
}
/* Inline Favorites Menu (Replaces #taglist in #gd4) */
#gd4.eh-fav-active {
height: auto !important;
max-height: 100% !important;
overflow: visible !important;
}
#gd4.eh-fav-active > :not(#eh-inline-favorites-container) {
display: none !important;
}
#eh-inline-favorites-container {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
padding: 0 4px 4px 4px !important;
margin: 0 !important;
font-family: arial, helvetica, sans-serif !important;
font-size: 9pt !important;
line-height: normal !important;
color: inherit !important;
overflow: visible !important;
}
#eh-inline-favorites-container p.eh-fav-header {
margin: 0 0 6px 0 !important;
padding: 0 !important;
text-align: center !important;
font-weight: normal !important;
font-size: 9pt !important;
line-height: 1.25 !important;
}
#eh-inline-favorites-container .eh-fav-body {
display: flex !important;
flex-direction: row !important;
justify-content: space-between !important;
align-items: flex-start !important;
gap: 12px !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
#eh-inline-favorites-container .eh-fav-cats {
display: flex !important;
flex-direction: column !important;
flex: 0 0 auto !important;
width: auto !important;
min-width: 140px !important;
max-width: 165px !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 0 !important;
gap: 2px !important;
}
#eh-inline-favorites-container .eh-fav-cat-item {
display: flex !important;
align-items: center !important;
cursor: pointer !important;
user-select: none !important;
padding: 1px 4px !important;
border-radius: 3px !important;
gap: 6px !important;
height: 20px !important;
box-sizing: border-box !important;
}
#eh-inline-favorites-container .eh-fav-cat-item:hover {
background: rgba(255, 255, 255, 0.08) !important;
}
.eh-theme-light #eh-inline-favorites-container .eh-fav-cat-item:hover {
background: rgba(0, 0, 0, 0.06) !important;
}
#eh-inline-favorites-container .eh-fav-cat-item input[type="radio"] {
margin: 0 !important;
cursor: pointer !important;
flex-shrink: 0 !important;
}
#eh-inline-favorites-container .eh-fav-cat-diamond,
#eh-inline-favorites-container .i {
width: 19px !important;
height: 19px !important;
min-width: 19px !important;
min-height: 19px !important;
background-image: url('https://ehgt.org/g/fav.png') !important;
background-repeat: no-repeat !important;
display: inline-block !important;
vertical-align: middle !important;
flex-shrink: 0 !important;
box-sizing: border-box !important;
}
#eh-inline-favorites-container .eh-fav-cat-label {
font-size: 9pt !important;
font-weight: normal !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
#eh-inline-favorites-container .eh-fav-note-pane {
display: flex !important;
flex-direction: column !important;
flex: 1 1 0% !important;
min-width: 0 !important;
max-width: 480px !important;
margin-right: 35px !important; /* Space on the right so it doesn't touch the panel edge */
box-sizing: border-box !important;
}
#eh-inline-favorites-container .eh-fav-note-title {
font-weight: bold !important;
text-align: center !important;
margin-bottom: 4px !important;
font-size: 9pt !important;
}
#eh-inline-favorites-container textarea.eh-fav-note-textarea {
width: 100% !important;
height: 125px !important;
min-height: 110px !important;
max-height: 135px !important;
box-sizing: border-box !important;
resize: none !important;
font-family: inherit !important;
font-size: 9pt !important;
padding: 6px 8px !important;
border-radius: 3px !important;
outline: none !important;
}
.eh-theme-dark #eh-inline-favorites-container textarea.eh-fav-note-textarea {
background-color: #27282c !important;
color: #eeeeee !important;
border: 1px solid #4f535b !important;
}
.eh-theme-light #eh-inline-favorites-container textarea.eh-fav-note-textarea {
background-color: #fcfbfa !important;
color: #333333 !important;
border: 1px solid #b5a4a4 !important;
}
#eh-inline-favorites-container .eh-fav-slots {
margin: 4px 0 0 0 !important;
text-align: right !important;
font-size: 8pt !important;
opacity: 0.9 !important;
}
#eh-inline-favorites-container .eh-fav-actions {
display: flex !important;
align-items: center !important;
justify-content: center !important;
gap: 10px !important;
margin-top: 8px !important;
width: 100% !important;
}
#eh-inline-favorites-container input[type="submit"],
#eh-inline-favorites-container input[type="button"],
#eh-inline-favorites-container .eh-fav-submit-btn,
#eh-inline-favorites-container .eh-fav-remove-btn,
#eh-inline-favorites-container .eh-fav-cancel-btn {
font-family: arial, helvetica, sans-serif !important;
font-size: 9pt !important;
font-weight: bold !important;
min-height: 23px !important;
line-height: 17px !important;
padding: 2px 14px !important;
border-radius: 3px !important;
cursor: pointer !important;
box-sizing: border-box !important;
outline: none !important;
transition: background-color 0.1s ease, border-color 0.1s ease !important;
}
.eh-theme-light #eh-inline-favorites-container input[type="submit"],
.eh-theme-light #eh-inline-favorites-container input[type="button"],
.eh-theme-light #eh-inline-favorites-container .eh-fav-submit-btn,
.eh-theme-light #eh-inline-favorites-container .eh-fav-remove-btn,
.eh-theme-light #eh-inline-favorites-container .eh-fav-cancel-btn {
background-color: #EDEADA !important;
color: #5C0D12 !important;
border: 2px solid #B5A4A4 !important;
}
.eh-theme-light #eh-inline-favorites-container input[type="submit"]:hover,
.eh-theme-light #eh-inline-favorites-container input[type="button"]:hover,
.eh-theme-light #eh-inline-favorites-container .eh-fav-submit-btn:hover,
.eh-theme-light #eh-inline-favorites-container .eh-fav-remove-btn:hover,
.eh-theme-light #eh-inline-favorites-container .eh-fav-cancel-btn:hover {
background-color: #F3F0E0 !important;
border-color: #977273 !important;
color: #5C0D12 !important;
}
.eh-theme-dark #eh-inline-favorites-container input[type="submit"],
.eh-theme-dark #eh-inline-favorites-container input[type="button"],
.eh-theme-dark #eh-inline-favorites-container .eh-fav-submit-btn,
.eh-theme-dark #eh-inline-favorites-container .eh-fav-remove-btn,
.eh-theme-dark #eh-inline-favorites-container .eh-fav-cancel-btn {
background-color: #34353b !important;
color: #dddddd !important;
border: 2px solid #8d8d8d !important;
}
.eh-theme-dark #eh-inline-favorites-container input[type="submit"]:hover,
.eh-theme-dark #eh-inline-favorites-container input[type="button"]:hover,
.eh-theme-dark #eh-inline-favorites-container .eh-fav-submit-btn:hover,
.eh-theme-dark #eh-inline-favorites-container .eh-fav-remove-btn:hover,
.eh-theme-dark #eh-inline-favorites-container .eh-fav-cancel-btn:hover {
background-color: #43464e !important;
border-color: #aeaeae !important;
color: #ffffff !important;
}
/* Suppress foreign/ComicRead quick-favorite popup injected into #gd3 */
#gd3 > :not(#gdc):not(#gdn):not(#gdd):not(#gdr):not(#gdf):not(.c) {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
opacity: 0 !important;
height: 0 !important;
width: 0 !important;
overflow: hidden !important;
}
.ido,
div.ido {
position: relative !important;
}
/* Relocated #favact to the top right of .ido (light gray box) */
#eh-favact-top,
#favact.eh-favact-top,
.eh-favact-top {
display: inline-flex !important;
justify-content: flex-end !important;
gap: 6px !important;
position: absolute !important;
top: 6px !important;
right: 0px !important;
float: none !important;
clear: none !important;
margin: 0 !important;
padding: 0 !important;
text-align: right !important;
box-sizing: border-box !important;
white-space: nowrap !important;
width: auto !important;
height: auto !important;
line-height: normal !important;
z-index: 100 !important;
}
#favact.eh-favact-top select,
.eh-favact-top select {
font-family: inherit !important;
font-size: 9pt !important;
box-sizing: border-box !important;
margin: 0 !important;
vertical-align: middle !important;
height: 26px !important;
min-height: 26px !important;
line-height: 20px !important;
border-radius: 3px !important;
}
#favact.eh-favact-top input[type="submit"],
#favact.eh-favact-top input[type="button"],
#favact.eh-favact-top input[value="Apply"],
#favact.eh-favact-top input[value="Confirm"],
#favact.eh-favact-top button,
.eh-favact-top input[type="submit"],
.eh-favact-top input[type="button"],
.eh-favact-top input[value="Apply"],
.eh-favact-top input[value="Confirm"],
.eh-favact-top button {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
font-family: arial, helvetica, sans-serif !important;
font-size: 9pt !important;
font-weight: bold !important;
padding: 1px 12px 2px !important;
margin: 0 0 0 2px !important;
border-radius: 3px !important;
cursor: pointer !important;
box-sizing: border-box !important;
vertical-align: middle !important;
height: 26px !important;
min-height: 26px !important;
line-height: 20px !important;
white-space: nowrap !important;
transition: background-color 0.1s ease, border-color 0.1s ease !important;
}
/* Checkbox aligned horizontally with the dropdown and button */
#alltoggle,
[name="alltoggle"],
.alltoggle,
#favact input[type="checkbox"],
.eh-favact-top input[type="checkbox"],
#favact.eh-favact-top input[type="checkbox"],
#eh-favact-top input[type="checkbox"] {
margin: 0 5px 0 0 !important;
padding: 0 !important;
vertical-align: middle !important;
position: static !important;
top: 0 !important;
cursor: pointer !important;
align-self: center !important;
}
#favact.eh-favact-top label,
#favact.eh-favact-top span,
.eh-favact-top label,
.eh-favact-top span,
#eh-favact-top label,
#eh-favact-top span {
display: inline-flex !important;
align-items: center !important;
vertical-align: middle !important;
font-size: 9pt !important;
line-height: normal !important;
}
#favact.eh-favact-top div,
.eh-favact-top div,
#eh-favact-top div {
display: inline-flex !important;
vertical-align: middle !important;
font-size: 9pt !important;
line-height: normal !important;
}
/* Light Theme (E-Hentai) */
.eh-theme-light #favact.eh-favact-top select,
.eh-theme-light .eh-favact-top select {
background-color: #EDEADA !important;
color: #5C0D12 !important;
border: 2px solid #B5A4A4 !important;
}
.eh-theme-light #favact.eh-favact-top input[type="submit"],
.eh-theme-light #favact.eh-favact-top input[type="button"],
.eh-theme-light #favact.eh-favact-top input[value="Apply"],
.eh-theme-light #favact.eh-favact-top input[value="Confirm"],
.eh-theme-light #favact.eh-favact-top button,
.eh-theme-light .eh-favact-top input[type="submit"],
.eh-theme-light .eh-favact-top input[type="button"],
.eh-theme-light .eh-favact-top input[value="Apply"],
.eh-theme-light .eh-favact-top input[value="Confirm"],
.eh-theme-light .eh-favact-top button {
background-color: #EDEADA !important;
color: #5C0D12 !important;
border: 2px solid #B5A4A4 !important;
}
.eh-theme-light #favact.eh-favact-top input[type="submit"]:hover,
.eh-theme-light #favact.eh-favact-top input[type="button"]:hover,
.eh-theme-light #favact.eh-favact-top input[value="Apply"]:hover,
.eh-theme-light #favact.eh-favact-top input[value="Confirm"]:hover,
.eh-theme-light #favact.eh-favact-top button:hover,
.eh-theme-light .eh-favact-top input[type="submit"]:hover,
.eh-theme-light .eh-favact-top input[type="button"]:hover,
.eh-theme-light .eh-favact-top input[value="Apply"]:hover,
.eh-theme-light .eh-favact-top input[value="Confirm"]:hover,
.eh-theme-light .eh-favact-top button:hover {
background-color: #F3F0E0 !important;
border-color: #977273 !important;
color: #5C0D12 !important;
}
/* Dark Theme (ExHentai) */
.eh-theme-dark #favact.eh-favact-top select,
.eh-theme-dark .eh-favact-top select {
background-color: #34353b !important;
color: #dddddd !important;
border: 2px solid #8d8d8d !important;
}
.eh-theme-dark #favact.eh-favact-top input[type="submit"],
.eh-theme-dark #favact.eh-favact-top input[type="button"],
.eh-theme-dark #favact.eh-favact-top input[value="Apply"],
.eh-theme-dark #favact.eh-favact-top input[value="Confirm"],
.eh-theme-dark #favact.eh-favact-top button,
.eh-theme-dark .eh-favact-top input[type="submit"],
.eh-theme-dark .eh-favact-top input[type="button"],
.eh-theme-dark .eh-favact-top input[value="Apply"],
.eh-theme-dark .eh-favact-top input[value="Confirm"],
.eh-theme-dark .eh-favact-top button {
background-color: #34353b !important;
color: #dddddd !important;
border: 2px solid #8d8d8d !important;
}
.eh-theme-dark #favact.eh-favact-top input[type="submit"]:hover,
.eh-theme-dark #favact.eh-favact-top input[type="button"]:hover,
.eh-theme-dark #favact.eh-favact-top input[value="Apply"]:hover,
.eh-theme-dark #favact.eh-favact-top input[value="Confirm"]:hover,
.eh-theme-dark #favact.eh-favact-top button:hover,
.eh-theme-dark .eh-favact-top input[type="submit"]:hover,
.eh-theme-dark .eh-favact-top input[type="button"]:hover,
.eh-theme-dark .eh-favact-top input[value="Apply"]:hover,
.eh-theme-dark .eh-favact-top input[value="Confirm"]:hover,
.eh-theme-dark .eh-favact-top button:hover {
background-color: #43464e !important;
border-color: #aeaeae !important;
color: #ffffff !important;
}
`;
function injectStyles() {
const style = document.createElement('style');
style.id = 'eh-fast-reader-core-css';
style.textContent = CORE_CSS;
const root = document.head || document.documentElement;
if (root) {
root.appendChild(style);
} else {
const observer = new MutationObserver(() => {
const el = document.head || document.documentElement;
if (el) {
el.appendChild(style);
observer.disconnect();
}
});
observer.observe(document, { childList: true });
}
}
injectStyles();
const isReaderUrl = location.pathname.startsWith('/s/');
let earlyStripActive = true;
try {
earlyStripActive = localStorage.getItem('eh_strip_mode_enabled') !== '0';
} catch {}
let earlyLayout = 'horizontal';
try {
earlyLayout = localStorage.getItem('eh_strip_layout') || 'horizontal';
} catch {}
const isEarlyDark = location.hostname.includes('exhentai');
if (document.documentElement) {
document.documentElement.classList.add(isEarlyDark ? 'eh-theme-dark' : 'eh-theme-light');
if (isReaderUrl && earlyStripActive) {
document.documentElement.classList.add('eh-strip-mode-active', 'strip-' + earlyLayout);
}
}
// =========================================================================
// 1. MAIN-WORLD INJECTION
// =========================================================================
function injectMainWorldEngine() {
const run = () => {
let injected = false;
try {
const script = document.createElement('script');
script.textContent = `(${mainWorldCode.toString()})();`;
(document.head || document.documentElement).appendChild(script);
script.remove();
injected = true;
} catch (e) {
console.warn('ExFastReader: Script injection failed, falling back to direct execution:', e);
}
if (!injected && typeof mainWorldCode === 'function') {
mainWorldCode();
}
};
if (document.head || document.documentElement) {
run();
} else {
const observer = new MutationObserver(() => {
if (document.head || document.documentElement) {
observer.disconnect();
run();
}
});
observer.observe(document, { childList: true });
}
}
injectMainWorldEngine();
// =========================================================================
// 2. INCOGNITO SETTINGS AUTO-SYNC (FAST-LANE)
// =========================================================================
function getCookie(name) {
const matches = document.cookie.match(
new RegExp('(?:^|; )' + name.replace(/([\.$?*|{}\(\)\[\]\/\+^])/g, '\\$1') + '=([^;]*)')
);
return matches ? decodeURIComponent(matches[1]) : undefined;
}
function getSavedUconfig() {
try {
if (typeof GM_getValue === 'function') {
const v = GM_getValue('eh_saved_uconfig');
if (v && typeof v === 'string') return v;
}
} catch {}
try {
const v = localStorage.getItem('eh_saved_uconfig');
if (v && typeof v === 'string') return v;
} catch {}
return null;
}
function setSavedUconfig(val) {
if (!val || typeof val !== 'string') return;
try {
if (typeof GM_setValue === 'function') {
GM_setValue('eh_saved_uconfig', val);
}
} catch {}
try {
localStorage.setItem('eh_saved_uconfig', val);
} catch {}
}
async function checkAndSyncSettings() {
// If currently on the settings page, capture and save the latest uconfig cookie
if (location.pathname.startsWith('/uconfig')) {
const cur = getCookie('uconfig');
if (cur) setSavedUconfig(cur);
return;
}
const isReader = location.pathname.startsWith('/s/');
const syncCookieName = 'eh_uconfig_synced';
const syncKey = 'eh_uconfig_synced_' + location.hostname;
let memberId, uconfig, syncAttempted;
try {
memberId = getCookie('ipb_member_id');
uconfig = getCookie('uconfig');
syncAttempted = getCookie(syncCookieName) || sessionStorage.getItem(syncKey);
} catch {
return;
}
// Always update saved cache if uconfig is present
if (uconfig) {
setSavedUconfig(uconfig);
return;
}
if (!memberId || memberId === '0' || syncAttempted) {
return;
}
// FAST LANE: If we have a cached uconfig string in storage, inject it instantly without fetching!
const saved = getSavedUconfig();
const cookieDomain = '.' + location.hostname.replace(/^.*?(e-hentai\.org|exhentai\.org)$/, '$1');
if (saved) {
document.cookie = `uconfig=${saved}; path=/; domain=${cookieDomain}; SameSite=Lax; max-age=31536000`;
document.cookie = `${syncCookieName}=1; path=/; SameSite=Lax`;
try { sessionStorage.setItem(syncKey, 'done'); } catch {}
if (!isReader) {
location.replace(location.href);
}
return;
}
// On reader pages, silently sync in background without reloading
if (isReader) {
try {
sessionStorage.setItem(syncKey, 'attempted');
document.cookie = `${syncCookieName}=1; path=/; SameSite=Lax`;
fetch(location.origin + '/uconfig.php', {
method: 'GET',
credentials: 'include',
priority: 'high'
}).then((res) => {
if (res.ok) {
sessionStorage.setItem(syncKey, 'done');
document.cookie = `${syncCookieName}=1; path=/; SameSite=Lax`;
const newU = getCookie('uconfig');
if (newU) setSavedUconfig(newU);
}
}).catch(() => {});
} catch {}
return;
}
// COLD SYNC (First time with no cached uconfig):
// Prevent unstyled flash while fetching
let blocker = null;
try {
blocker = document.createElement('style');
blocker.id = 'eh-fast-sync-blocker';
blocker.textContent = 'html { opacity: 0 !important; }';
(document.head || document.documentElement).appendChild(blocker);
setTimeout(() => { if (blocker && blocker.parentNode) blocker.remove(); }, 1800);
} catch {}
try {
sessionStorage.setItem(syncKey, 'attempted');
document.cookie = `${syncCookieName}=1; path=/; SameSite=Lax`;
const res = await fetch(location.origin + '/uconfig.php', {
method: 'GET',
credentials: 'include',
priority: 'high'
});
if (res.ok) {
sessionStorage.setItem(syncKey, 'done');
document.cookie = `${syncCookieName}=1; path=/; SameSite=Lax`;
const newU = getCookie('uconfig');
if (newU) setSavedUconfig(newU);
location.replace(location.href);
} else if (blocker) {
blocker.remove();
}
} catch {
if (blocker) blocker.remove();
}
}
checkAndSyncSettings();
// =========================================================================
// 3. CORE BACKGROUND ENGINE & BIDIRECTIONAL STRIP READER
// =========================================================================
function mainWorldCode() {
if (window.__exFastReaderLoaded) return;
window.__exFastReaderLoaded = true;
const isReaderPage = location.pathname.startsWith('/s/');
const isGalleryPage = location.pathname.startsWith('/g/');
const isIndexPage = !isReaderPage && !isGalleryPage && (
location.pathname === '/' ||
location.pathname === '' ||
location.pathname.startsWith('/watched') ||
location.pathname.startsWith('/favorites') ||
location.pathname.startsWith('/popular') ||
location.pathname.startsWith('/tag/')
);
const PREFETCH_DEPTH = 3;
const IMAGE_TIMEOUT_MS = 10000;
const MAX_RETRIES = 3;
const MAX_CONCURRENT_LOADS = 2;
function safeStorageGet(storage, key, fallback = null) {
try {
const val = storage.getItem(key);
return val !== null ? val : fallback;
} catch {
return fallback;
}
}
function safeStorageSet(storage, key, val) {
try {
storage.setItem(key, val);
} catch {}
}
function safeStorageRemove(storage, key) {
try {
storage.removeItem(key);
} catch {}
}
const pageRespCache = new Map();
const pageImgKeyCache = new Map();
const pageImgSrcCache = new Map();
const pageNlCodes = new Map();
const preloadedImages = new Map();
const inFlightFetches = new Map();
const inFlightGalleryFetches = new Map();
const fetchedGalleryPages = new Map();
const pageDimensions = new Map();
const pageItemMap = new Map();
const loadedImages = new Set();
const loadingPages = new Map(); // pageNo -> startTime
const failedPages = new Map();
const pageTopBarInfoCache = new Map();
const pageFileSizeCache = new Map();
const downloadSpeedSamples = [];
const MAX_SPEED_SAMPLES = 8;
const pageThumbnailCache = new Map();
const spriteImagePromiseCache = new Map();
const spriteEntriesMap = new Map();
const spriteDimensionsCache = new Map();
let cachedGid = null;
let cachedShowkey = null;
let cachedApiUrl = null;
let lastSyncedIndicatorPage = -1;
let thumbsPerPage = parseInt(safeStorageGet(localStorage, 'eh_thumbs_per_page', '0'), 10) || 40;
let defaultAspectRatio = 0.707;
let cachedTotalPages = 0;
let stallTimer = null;
let isReloading = false;
let currentPageNum = 1;
let lastSingleTurnTime = 0;
function parseFileSizeInBytes(rawStr) {
if (!rawStr || typeof rawStr !== 'string') return 0;
const parts = rawStr.split('::');
const targetStr = parts.length >= 3 ? parts[2] : rawStr;
const m = targetStr.match(/(\d+(?:\.\d+)?)\s*([kKMmGg]?[iI]?[bB]+)/i) ||
rawStr.match(/(\d+(?:\.\d+)?)\s*([kKMmGg][iI]?[bB]+)/i);
if (!m) return 0;
const num = parseFloat(m[1]);
if (isNaN(num) || num <= 0) return 0;
const unit = m[2].toUpperCase();
if (unit.startsWith('G')) return Math.round(num * 1024 * 1024 * 1024);
if (unit.startsWith('M')) return Math.round(num * 1024 * 1024);
if (unit.startsWith('K')) return Math.round(num * 1024);
if (unit.startsWith('B')) return Math.round(num);
return 0;
}
function getPageFileSize(pageNo) {
if (!pageNo) return 1.5 * 1024 * 1024;
if (pageFileSizeCache.has(pageNo)) {
const cached = pageFileSizeCache.get(pageNo);
if (cached > 0) return cached;
}
const topInfo = pageTopBarInfoCache.get(pageNo);
if (topInfo) {
const sz = parseFileSizeInBytes(topInfo);
if (sz > 0) {
pageFileSizeCache.set(pageNo, sz);
return sz;
}
}
if (pageRespCache.has(pageNo)) {
const resp = pageRespCache.get(pageNo);
const candidates = [resp.i, resp.d, resp.i5, resp.i2, resp.i4, resp.i1, resp.i3];
for (const c of candidates) {
if (!c) continue;
const sz = parseFileSizeInBytes(c);
if (sz > 0) {
pageFileSizeCache.set(pageNo, sz);
return sz;
}
}
}
if (pageNo === currentPageNum) {
const raw = (document.getElementById('i2')?.textContent || '') + ' ' +
(document.getElementById('i4')?.textContent || '') + ' ' +
(document.getElementById('i1')?.textContent || '');
const sz = parseFileSizeInBytes(raw);
if (sz > 0) {
pageFileSizeCache.set(pageNo, sz);
return sz;
}
}
const dim = pageDimensions.get(pageNo);
if (dim && dim.width > 0 && dim.height > 0) {
const estimated = Math.round(dim.width * dim.height * 0.35);
if (estimated > 100 * 1024) {
return estimated;
}
}
return 1.5 * 1024 * 1024;
}
function recordDownloadSpeedSample(pageNo, durationSec) {
if (!durationSec || durationSec <= 0.05) return;
const sizeBytes = getPageFileSize(pageNo);
if (!sizeBytes || sizeBytes <= 0) return;
const speed = sizeBytes / durationSec;
if (speed <= 0 || !isFinite(speed)) return;
downloadSpeedSamples.push({ speed, timestamp: Date.now() });
while (downloadSpeedSamples.length > MAX_SPEED_SAMPLES) {
downloadSpeedSamples.shift();
}
const currentSpeed = getCurrentDownloadSpeed();
safeStorageSet(sessionStorage, 'eh_last_speed', String(Math.round(currentSpeed)));
}
function getCurrentDownloadSpeed() {
const now = Date.now();
while (downloadSpeedSamples.length > 0 && now - downloadSpeedSamples[0].timestamp > 300000) {
downloadSpeedSamples.shift();
}
if (downloadSpeedSamples.length > 0) {
const sorted = downloadSpeedSamples.map(s => s.speed).sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
const medianSpeed = sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
return Math.max(100 * 1024, medianSpeed);
}
const saved = parseInt(safeStorageGet(sessionStorage, 'eh_last_speed', '0'), 10);
if (saved > 50 * 1024 && saved < 500 * 1024 * 1024) {
return saved;
}
if (typeof navigator !== 'undefined' && navigator.connection && typeof navigator.connection.downlink === 'number' && navigator.connection.downlink > 0) {
const bps = (navigator.connection.downlink * 1000 * 1000) / 8;
return Math.max(200 * 1024, bps);
}
return 1.5 * 1024 * 1024;
}
function calculateImageTimeout(pageNo) {
const fileSize = getPageFileSize(pageNo);
const speed = getCurrentDownloadSpeed();
const expectedTransferSec = fileSize / speed;
const baseLatencySec = 4.0;
const slowTimeoutMs = Math.max(6000, Math.round((baseLatencySec + expectedTransferSec * 2.2) * 1000));
const hardTimeoutMs = Math.max(12000, Math.round((baseLatencySec * 1.5 + expectedTransferSec * 4.0) * 1000));
const watchdogTimeoutMs = hardTimeoutMs + 5000;
return {
fileSize,
speed,
expectedTransferSec,
slowTimeoutMs,
hardTimeoutMs,
watchdogTimeoutMs
};
}
function normalizeApiUrl(url) {
const defaultApi = location.origin + '/api.php';
if (!url) return defaultApi;
try {
const parsed = new URL(url, location.origin);
const isAllowed =
parsed.origin === location.origin ||
parsed.hostname === 'api.e-hentai.org' ||
parsed.hostname === 'e-hentai.org' ||
parsed.hostname === 'exhentai.org' ||
parsed.hostname.endsWith('.e-hentai.org') ||
parsed.hostname.endsWith('.exhentai.org');
if (!isAllowed) {
return defaultApi;
}
return parsed.href;
} catch {
return defaultApi;
}
}
function safeParseJson(str) {
if (!str || typeof str !== 'string') return null;
const trimmed = str.trim();
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return null;
try {
return JSON.parse(trimmed);
} catch {
return null;
}
}
let _currentApiUrl = location.origin + '/api.php';
if (isReaderPage) {
try {
if (window.api_url) {
_currentApiUrl = normalizeApiUrl(window.api_url);
}
Object.defineProperty(window, 'api_url', {
configurable: true,
enumerable: true,
get() {
return _currentApiUrl;
},
set(val) {
_currentApiUrl = normalizeApiUrl(val);
}
});
} catch (e) {
window.api_url = _currentApiUrl;
}
if (typeof window.api_response === 'function' && !window.api_response.__eh_safe) {
const origApiResponse = window.api_response;
window.api_response = function (xhr) {
try {
if (xhr && typeof xhr.responseText === 'string') {
const parsed = safeParseJson(xhr.responseText);
if (parsed) return parsed;
return { error: xhr.responseText.trim() || 'Invalid response' };
}
return origApiResponse.apply(this, arguments);
} catch {
return { error: 'Invalid JSON response' };
}
};
window.api_response.__eh_safe = true;
}
}
function pruneGalleryLocalStorage(newGid) {
try {
let list = JSON.parse(safeStorageGet(localStorage, 'eh_gdata_recent', '[]') || '[]');
list = list.filter(id => id !== newGid);
list.unshift(newGid);
while (list.length > 8) {
const oldGid = list.pop();
safeStorageRemove(localStorage, 'eh_gdata_' + oldGid);
safeStorageRemove(localStorage, 'eh_gallery_' + oldGid);
safeStorageRemove(sessionStorage, 'eh_gdata_' + oldGid);
safeStorageRemove(sessionStorage, 'eh_gallery_' + oldGid);
}
safeStorageSet(localStorage, 'eh_gdata_recent', JSON.stringify(list));
} catch {}
}
function saveGalleryDataToSession(gid) {
if (!gid) return;
try {
const prevStr = safeStorageGet(sessionStorage, 'eh_gdata_' + gid) || safeStorageGet(localStorage, 'eh_gdata_' + gid);
const prev = prevStr ? JSON.parse(prevStr) : {
gid,
totalPages: cachedTotalPages || 0,
thumbsPerPage: thumbsPerPage || 40,
keys: {},
dims: {},
info: {},
thumbs: {},
spriteDims: {}
};
if (cachedTotalPages) prev.totalPages = cachedTotalPages;
if (thumbsPerPage) prev.thumbsPerPage = thumbsPerPage;
for (const [p, k] of pageImgKeyCache.entries()) {
prev.keys[p] = k;
}
for (const [p, d] of pageDimensions.entries()) {
prev.dims[p] = { w: d.width, h: d.height, ar: d.aspectRatio };
}
for (const [p, inf] of pageTopBarInfoCache.entries()) {
prev.info[p] = inf;
}
if (!prev.fileSizes) prev.fileSizes = {};
for (const [p, sz] of pageFileSizeCache.entries()) {
prev.fileSizes[p] = sz;
}
for (const [p, th] of pageThumbnailCache.entries()) {
prev.thumbs[p] = th;
}
for (const [url, sdim] of spriteDimensionsCache.entries()) {
prev.spriteDims[url] = sdim;
}
const serialized = JSON.stringify(prev);
safeStorageSet(sessionStorage, 'eh_gdata_' + gid, serialized);
safeStorageSet(localStorage, 'eh_gdata_' + gid, serialized);
pruneGalleryLocalStorage(gid);
} catch (err) {
console.warn('ExFastReader: Failed to save gallery session data', err);
}
}
function loadGalleryDataFromSession(gid) {
if (!gid) return false;
try {
let raw = safeStorageGet(sessionStorage, 'eh_gdata_' + gid);
if (!raw) {
raw = safeStorageGet(localStorage, 'eh_gdata_' + gid);
}
if (!raw) return false;
const data = JSON.parse(raw);
if (!data) return false;
if (data.totalPages && data.totalPages > cachedTotalPages) {
cachedTotalPages = data.totalPages;
}
if (data.thumbsPerPage) {
thumbsPerPage = data.thumbsPerPage;
}
if (data.keys) {
for (const [p, k] of Object.entries(data.keys)) {
pageImgKeyCache.set(parseInt(p, 10), k);
}
}
if (data.dims) {
for (const [p, d] of Object.entries(data.dims)) {
const page = parseInt(p, 10);
if (!pageDimensions.has(page) || pageDimensions.get(page).width === 0) {
pageDimensions.set(page, { width: d.w, height: d.h, aspectRatio: d.ar });
}
}
}
if (data.info) {
for (const [p, inf] of Object.entries(data.info)) {
pageTopBarInfoCache.set(parseInt(p, 10), inf);
}
}
if (data.fileSizes) {
for (const [p, sz] of Object.entries(data.fileSizes)) {
pageFileSizeCache.set(parseInt(p, 10), sz);
}
}
if (data.spriteDims) {
for (const [url, sdim] of Object.entries(data.spriteDims)) {
spriteDimensionsCache.set(url, sdim);
}
}
if (data.thumbs) {
for (const [p, th] of Object.entries(data.thumbs)) {
const page = parseInt(p, 10);
pageThumbnailCache.set(page, th);
}
}
const spriteUrls = new Set();
for (const th of pageThumbnailCache.values()) {
if (th && th.type === 'sprite' && th.url) {
spriteUrls.add(th.url);
}
}
for (const sUrl of spriteUrls) {
loadAndSliceSprite(sUrl);
}
return true;
} catch (err) {
console.warn('ExFastReader: Failed to load gallery session data', err);
return false;
}
}
let stripActive = safeStorageGet(localStorage, 'eh_strip_mode_enabled', '1') !== '0';
let currentLayout = safeStorageGet(localStorage, 'eh_strip_layout', 'horizontal');
function setStripClasses(active, layout) {
const root = document.documentElement;
const body = document.body;
const targets = body ? [root, body] : [root];
for (const el of targets) {
if (!el) continue;
if (active) {
el.classList.add('eh-strip-mode-active');
el.classList.remove('strip-horizontal', 'strip-vertical');
el.classList.add('strip-' + layout);
} else {
el.classList.remove('eh-strip-mode-active', 'strip-horizontal', 'strip-vertical', 'eh-topbar-visible');
}
}
}
if (isReaderPage) {
setStripClasses(stripActive, currentLayout);
if (!document.body && stripActive) {
let bodyObsTimer = null;
const bodyObs = new MutationObserver(() => {
if (document.body) {
if (bodyObsTimer) clearTimeout(bodyObsTimer);
setStripClasses(stripActive, currentLayout);
bodyObs.disconnect();
}
});
bodyObs.observe(document.documentElement, { childList: true });
bodyObsTimer = setTimeout(() => {
bodyObs.disconnect();
}, 10000);
}
}
const mInitialGidMatch = location.pathname.match(/\/s\/[0-9a-zA-Z]+\/(\d+)-/i) || location.pathname.match(/\/g\/(\d+)\//i);
if (mInitialGidMatch) {
cachedGid = parseInt(mInitialGidMatch[1], 10);
loadGalleryDataFromSession(cachedGid);
if (isReaderPage && stripActive) {
const initMeta = getCurrentPageMeta();
const initP = initMeta.page || 1;
const pDim = pageDimensions.get(initP) || pageDimensions.get(1) || (pageDimensions.size > 0 ? pageDimensions.values().next().value : null);
if (pDim && pDim.aspectRatio) {
defaultAspectRatio = pDim.aspectRatio;
applyGlobalAspectRatio(defaultAspectRatio);
}
}
}
let lastStripTurnTime = 0;
let scrollRafId = null;
let lastHistoryPage = -1;
let historyDebounceTimer = null;
let lastUpdatePage = -1;
let isPumpingQueue = false;
let isTransitioningLayout = false;
let isAnchoring = false;
let targetScrollLeft = null;
let animScrollRaf = null;
let targetScrollTop = null;
let animScrollTopRaf = null;
let cachedFrontUrl = location.origin + '/';
let cachedOnionUrl = '';
let cachedGalleryUrl = '';
let scrollDirection = 'forward';
let lastScrollY = typeof window !== 'undefined' ? window.scrollY : 0;
let lastScrollLeft = 0;
let stripItemObserver = null;
function getStripItemObserver() {
if (!stripItemObserver && typeof IntersectionObserver !== 'undefined') {
stripItemObserver = new IntersectionObserver((entries) => {
let shouldPump = false;
for (const entry of entries) {
if (entry.isIntersecting) {
const p = parseInt(entry.target.dataset.page, 10);
if (p) {
if (entry.target.classList.contains('eh-placeholder') && !entry.target.querySelector('.eh-placeholder-thumb-wrap')) {
applyThumbnailToPlaceholder(entry.target, p);
}
if (isPageEligibleToLoad(p)) {
shouldPump = true;
}
}
}
}
if (shouldPump) {
checkAndLoadNearbyPages();
}
}, {
root: currentLayout === 'horizontal' ? document.getElementById('eh-strip-container') : null,
rootMargin: '350px'
});
}
return stripItemObserver;
}
function cancelKeyboardAnimation() {
if (animScrollRaf) {
cancelAnimationFrame(animScrollRaf);
animScrollRaf = null;
}
if (animScrollTopRaf) {
cancelAnimationFrame(animScrollTopRaf);
animScrollTopRaf = null;
}
targetScrollLeft = null;
targetScrollTop = null;
}
function withScrollAnchor(fn, targetPageNo = null) {
if (!stripActive || isAnchoring || isTransitioningLayout) {
fn();
return;
}
const container = document.getElementById('eh-strip-container');
if (!container) {
fn();
return;
}
// If the page being updated is at or after the current visible page,
// it cannot shift earlier elements in RTL row or vertical column flex.
if (targetPageNo !== null && targetPageNo >= currentPageNum) {
fn();
return;
}
isAnchoring = true;
try {
let anchorEl = null;
let anchorRightBefore = 0;
let anchorTopBefore = 0;
const vpX = window.innerWidth / 2;
const vpY = window.innerHeight / 2;
let closestDist = Infinity;
for (const [p, item] of pageItemMap.entries()) {
if (Math.abs(p - currentPageNum) > 3) continue;
if (!item || !item.isConnected) continue;
const r = item.getBoundingClientRect();
if (currentLayout === 'horizontal') {
if (r.right > 0 && r.left < window.innerWidth) {
const center = (r.left + r.right) / 2;
const dist = Math.abs(center - vpX);
if (dist < closestDist) {
closestDist = dist;
anchorEl = item;
}
}
} else {
if (r.bottom > 0 && r.top < window.innerHeight) {
const center = (r.top + r.bottom) / 2;
const dist = Math.abs(center - vpY);
if (dist < closestDist) {
closestDist = dist;
anchorEl = item;
}
}
}
}
if (!anchorEl) {
anchorEl = pageItemMap.get(currentPageNum);
}
if (anchorEl && anchorEl.isConnected) {
const r = anchorEl.getBoundingClientRect();
anchorRightBefore = r.right;
anchorTopBefore = r.top;
}
fn();
if (anchorEl && anchorEl.isConnected && (anchorRightBefore || anchorTopBefore)) {
const rAfter = anchorEl.getBoundingClientRect();
if (currentLayout === 'horizontal') {
const deltaX = rAfter.right - anchorRightBefore;
if (Math.abs(deltaX) >= 4.0) {
const roundX = Math.round(deltaX);
container.scrollLeft += roundX;
lastScrollLeft = container.scrollLeft;
if (targetScrollLeft !== null) {
targetScrollLeft += roundX;
const maxScroll = 0;
const minScroll = -(container.scrollWidth - container.clientWidth);
targetScrollLeft = Math.max(minScroll, Math.min(maxScroll, targetScrollLeft));
}
}
} else {
const deltaY = rAfter.top - anchorTopBefore;
if (Math.abs(deltaY) >= 4.0) {
const roundY = Math.round(deltaY);
window.scrollBy(0, roundY);
lastScrollY = window.scrollY;
if (targetScrollTop !== null) {
targetScrollTop += roundY;
const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
targetScrollTop = Math.max(0, Math.min(maxScroll, targetScrollTop));
}
}
}
}
} finally {
isAnchoring = false;
}
}
function scrollByEvenIncrement(delta) {
if (delta < 0) {
scrollDirection = 'backward';
} else if (delta > 0) {
scrollDirection = 'forward';
}
if (stripActive && currentLayout === 'horizontal') {
const container = document.getElementById('eh-strip-container');
if (!container) return;
const maxScroll = 0;
const minScroll = -(container.scrollWidth - container.clientWidth);
if (targetScrollLeft === null || isNaN(targetScrollLeft)) {
targetScrollLeft = container.scrollLeft;
}
targetScrollLeft -= delta;
if (targetScrollLeft > maxScroll) targetScrollLeft = maxScroll;
if (targetScrollLeft < minScroll) targetScrollLeft = minScroll;
animateHorizontalScroll(container);
} else {
const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
let targetTop = (targetScrollTop !== null && !isNaN(targetScrollTop)) ? targetScrollTop : window.scrollY;
targetTop += delta;
if (targetTop < 0) targetTop = 0;
if (targetTop > maxScroll) targetTop = maxScroll;
targetScrollTop = targetTop;
animateVerticalScroll();
}
}
function animateHorizontalScroll(container) {
if (animScrollRaf) return;
function step() {
if (targetScrollLeft === null) {
animScrollRaf = null;
return;
}
const current = container.scrollLeft;
const diff = targetScrollLeft - current;
if (Math.abs(diff) <= 1) {
container.scrollLeft = targetScrollLeft;
targetScrollLeft = null;
animScrollRaf = null;
scheduleScrollUpdate();
return;
}
const move = diff * 0.28;
const nextVal = current + (Math.abs(move) < 1 ? Math.sign(diff) : move);
container.scrollLeft = nextVal;
if (container.scrollLeft === current) {
targetScrollLeft = null;
animScrollRaf = null;
scheduleScrollUpdate();
return;
}
scheduleScrollUpdate();
animScrollRaf = requestAnimationFrame(step);
}
animScrollRaf = requestAnimationFrame(step);
}
function animateVerticalScroll() {
if (animScrollTopRaf) return;
function step() {
if (targetScrollTop === null) {
animScrollTopRaf = null;
return;
}
const current = window.scrollY;
const diff = targetScrollTop - current;
if (Math.abs(diff) <= 1) {
window.scrollTo(0, targetScrollTop);
targetScrollTop = null;
animScrollTopRaf = null;
scheduleScrollUpdate();
return;
}
const move = diff * 0.28;
const nextVal = current + (Math.abs(move) < 1 ? Math.sign(diff) : move);
window.scrollTo(0, nextVal);
if (window.scrollY === current) {
targetScrollTop = null;
animScrollTopRaf = null;
scheduleScrollUpdate();
return;
}
scheduleScrollUpdate();
animScrollTopRaf = requestAnimationFrame(step);
}
animScrollTopRaf = requestAnimationFrame(step);
}
function extractAndHideBottomLinks() {
let frontUrl = location.origin + '/';
let onionUrl = '';
if (location.hostname.includes('exhentai')) {
onionUrl = 'http://exhentai55ld2wyap5juskbm67czulomrouspvlunwfuhqack263eccd.onion';
} else {
onionUrl = 'http://e-hentai.org';
}
const footerLinks = document.querySelectorAll('#footer a, .ft a, .dp a, #dp a');
for (const a of footerLinks) {
const href = a.getAttribute('href') || a.href || '';
const txt = a.textContent.trim().toLowerCase();
if (href.includes('wiki') || href.includes('forum')) continue;
if (txt === 'front page' || txt === 'front' || txt.startsWith('front page')) {
try {
const u = new URL(href, location.origin);
if (u.origin === location.origin) frontUrl = u.href;
} catch {}
}
if (txt.includes('onion') || txt.includes('tor') || href.includes('.onion')) {
if (href) onionUrl = href;
}
}
cachedFrontUrl = frontUrl;
cachedOnionUrl = onionUrl;
return { frontUrl, onionUrl };
}
function syncThemeClass() {
const isDark =
location.hostname.includes('exhentai') ||
(document.body &&
window.getComputedStyle(document.body).backgroundColor.includes('52') &&
window.getComputedStyle(document.body).backgroundColor.includes('53'));
document.documentElement.classList.remove('eh-theme-dark', 'eh-theme-light');
document.documentElement.classList.add(isDark ? 'eh-theme-dark' : 'eh-theme-light');
}
function showFailureToast(msg) {
let toast = document.getElementById('eh-toast-fail');
if (!toast) {
toast = document.createElement('div');
toast.id = 'eh-toast-fail';
toast.className = 'eh-toast-fail';
document.body.appendChild(toast);
toast.addEventListener('click', () => {
safeStorageRemove(sessionStorage, 'eh_retry_' + location.pathname);
toast.remove();
executeBrokenReload();
});
}
toast.textContent = msg;
}
function parseTemplate(html) {
const t = document.createElement('template');
t.innerHTML = html;
return t.content;
}
function getBaseGalleryUrl() {
if (cachedGalleryUrl && /\/g\/\d+\/[0-9a-fA-F]{10}/i.test(cachedGalleryUrl)) {
return cachedGalleryUrl.replace(/\?.*$/, '').replace(/\/?$/, '/');
}
const mPath = location.pathname.match(/\/s\/[0-9a-zA-Z]+\/(\d+)-/i);
const curGid = mPath ? mPath[1] : (getPageParams()?.gid || '');
if (curGid) {
const stored = safeStorageGet(sessionStorage, 'eh_gallery_' + curGid) || safeStorageGet(localStorage, 'eh_gallery_' + curGid);
if (stored && /\/g\/\d+\/[0-9a-fA-F]{10}/i.test(stored)) {
cachedGalleryUrl = stored.replace(/\?.*$/, '').replace(/\/?$/, '/');
return cachedGalleryUrl;
}
}
// 1. Check all <a> tags for exact /g/{gid}/{token}/ link
const links = document.querySelectorAll('a[href*="/g/"]');
for (const a of links) {
const href = a.getAttribute('href') || a.href || '';
const m = href.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
if (m) {
if (!curGid || m[1] === String(curGid)) {
cachedGalleryUrl = `${location.origin}/g/${m[1]}/${m[2]}/`;
if (curGid) { safeStorageSet(sessionStorage, 'eh_gallery_' + curGid, cachedGalleryUrl); safeStorageSet(localStorage, 'eh_gallery_' + curGid, cachedGalleryUrl); }
return cachedGalleryUrl;
}
}
}
// 2. Check document.referrer
if (document.referrer) {
const mRef = document.referrer.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
if (mRef && (!curGid || mRef[1] === String(curGid))) {
cachedGalleryUrl = `${location.origin}/g/${mRef[1]}/${mRef[2]}/`;
if (curGid) { safeStorageSet(sessionStorage, 'eh_gallery_' + curGid, cachedGalleryUrl); safeStorageSet(localStorage, 'eh_gallery_' + curGid, cachedGalleryUrl); }
return cachedGalleryUrl;
}
}
// 3. Scan scripts without whole-DOM serialization
for (const s of document.scripts) {
const txt = s.textContent || '';
if (curGid) {
const rGid = new RegExp('/g/' + curGid + '/([0-9a-fA-F]{10})', 'i');
const mGid = txt.match(rGid);
if (mGid) {
cachedGalleryUrl = `${location.origin}/g/${curGid}/${mGid[1]}/`;
safeStorageSet(sessionStorage, 'eh_gallery_' + curGid, cachedGalleryUrl);
return cachedGalleryUrl;
}
}
const mAny = txt.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
if (mAny) {
cachedGalleryUrl = `${location.origin}/g/${mAny[1]}/${mAny[2]}/`;
if (curGid) { safeStorageSet(sessionStorage, 'eh_gallery_' + curGid, cachedGalleryUrl); safeStorageSet(localStorage, 'eh_gallery_' + curGid, cachedGalleryUrl); }
return cachedGalleryUrl;
}
}
return '';
}
function detectInitialThumbsPerPage() {
const curMeta = getCurrentPageMeta();
const page = curMeta.page;
const refUrl = document.referrer || '';
let foundP = null;
const returnLinks = document.querySelectorAll('a[href*="/g/"]');
for (const a of returnLinks) {
const href = a.getAttribute('href') || a.href || '';
const m = href.match(/[?&]p=(\d+)/);
if (m) {
foundP = parseInt(m[1], 10);
break;
}
}
if (foundP === null && refUrl) {
const m = refUrl.match(/[?&]p=(\d+)/);
if (m) foundP = parseInt(m[1], 10);
}
if (foundP !== null && page > 1) {
for (const count of [20, 40, 100]) {
if (Math.floor((page - 1) / count) === foundP) {
thumbsPerPage = count;
safeStorageSet(localStorage, 'eh_thumbs_per_page', String(count));
break;
}
}
}
}
function getGalleryUrl(pageNo = null) {
const base = getBaseGalleryUrl();
if (!base) return '';
const targetPage = pageNo || currentPageNum || 1;
const count = (typeof thumbsPerPage === 'number' && thumbsPerPage > 0) ? thumbsPerPage : 40;
const pageIndex = Math.max(0, Math.floor((targetPage - 1) / count));
try {
const u = new URL(base, location.origin);
if (pageIndex > 0) {
u.searchParams.set('p', String(pageIndex));
} else {
u.searchParams.delete('p');
}
return u.href;
} catch {
const cleanBase = base.replace(/[?&]p=\d+/g, '').replace(/\?$/, '');
return pageIndex > 0 ? (cleanBase + (cleanBase.includes('?') ? '&' : '?') + 'p=' + pageIndex) : cleanBase;
}
}
function getCurrentPageMeta() {
const m = location.pathname.match(/\/s\/([0-9a-zA-Z]+)\/\d+-(\d+)/i);
let page = m ? parseInt(m[2], 10) : 0;
let key = m ? m[1] : '';
if (!page && typeof window.currentpage === 'number' && window.currentpage > 0) {
page = window.currentpage;
}
if (!page && typeof window.startpage === 'number' && window.startpage > 0) {
page = window.startpage;
}
if (!key && typeof window.currentkey === 'string' && window.currentkey) {
key = window.currentkey;
}
if (!key && typeof window.startkey === 'string' && window.startkey) {
key = window.startkey;
}
if (!page || isNaN(page)) {
const curSpan = document.querySelector('#i2 .sn span:first-child, #i4 .sn span:first-child');
if (curSpan) {
const p = parseInt(curSpan.textContent.trim(), 10);
if (!isNaN(p)) page = p;
}
}
return { page: Math.max(1, page || 1), imgKey: key };
}
function applyGlobalAspectRatio(ar) {
if (!ar || isNaN(ar) || ar <= 0) return;
document.documentElement.style.setProperty('--eh-ar', String(ar));
const container = document.getElementById('eh-strip-container');
if (container) {
container.style.setProperty('--eh-ar', String(ar));
}
}
function extractNlCode(source = document) {
if (!source) return null;
if (typeof source === 'object' && !(source instanceof Node)) {
if (typeof source.nl === 'string' && source.nl.trim()) return source.nl.trim();
const searchSources = [source.i6, source.i5, source.i7, source.i3, source.i2, source.i4, source.i1, source.n];
for (const s of searchSources) {
if (!s || typeof s !== 'string') continue;
const m = s.match(/nl\s*\(\s*['"]\s*([^'"]+?)\s*['"]\s*\)/i) ||
s.match(/id=["']loadfail["'][^>]*onclick=["']return\s*nl\(['"]([^'"]+)['"]\)/i) ||
s.match(/[?&]nl=([0-9a-zA-Z\-_]+)/i);
if (m) return m[1].trim();
}
try {
const raw = JSON.stringify(source);
const mRaw = raw.match(/nl\s*(?:\\(|\()\s*['"\ ]*([0-9a-zA-Z\-_]+)['"\ ]*\s*(?:\\)|\))/i) ||
raw.match(/[?&]nl=([0-9a-zA-Z\-_]+)/i);
if (mRaw) return mRaw[1].trim();
} catch {}
return null;
}
const doc = source;
const loadfail = doc.getElementById ? doc.getElementById('loadfail') : null;
if (loadfail) {
const m = (loadfail.getAttribute('onclick') || '').match(/nl\s*\(\s*['"]\s*([^'"]+?)\s*['"]\s*\)/i);
if (m) return m[1].trim();
const mHref = (loadfail.getAttribute('href') || '').match(/[?&]nl=([0-9a-zA-Z\-_]+)/i);
if (mHref) return mHref[1].trim();
}
const img = doc.getElementById ? doc.getElementById('img') : null;
if (img) {
const m = (img.getAttribute('onerror') || '').match(/nl\s*\(\s*['"]\s*([^'"]+?)\s*['"]\s*\)/i);
if (m) return m[1].trim();
}
const allNlLinks = doc.querySelectorAll ? doc.querySelectorAll('a[onclick*="nl("]') : [];
for (const a of allNlLinks) {
const m = (a.getAttribute('onclick') || '').match(/nl\s*\(\s*['"]\s*([^'"]+?)\s*['"]\s*\)/i);
if (m) return m[1].trim();
}
const i3 = doc.getElementById ? doc.getElementById('i3') : null;
if (i3) {
const m = i3.innerHTML.match(/nl\s*\(\s*['"]\s*([^'"]+?)\s*['"]\s*\)/i);
if (m) return m[1].trim();
}
const i6 = doc.getElementById ? doc.getElementById('i6') : null;
if (i6) {
const m = i6.innerHTML.match(/nl\s*\(\s*['"]\s*([^'"]+?)\s*['"]\s*\)/i);
if (m) return m[1].trim();
}
return null;
}
const extractInitialNlCode = extractNlCode;
function detectInitialPageDimensions() {
if (pageDimensions.has(currentPageNum) && pageDimensions.get(currentPageNum).aspectRatio) {
defaultAspectRatio = pageDimensions.get(currentPageNum).aspectRatio;
applyGlobalAspectRatio(defaultAspectRatio);
return;
}
const curImg = document.getElementById('img');
if (curImg) {
const sw = parseInt(curImg.style.width, 10);
const sh = parseInt(curImg.style.height, 10);
if (sw > 0 && sh > 0) {
const ar = sw / sh;
if (ar >= 0.3 && ar <= 2.5) {
defaultAspectRatio = ar > 1.05 ? (ar / 2) : ar;
pageDimensions.set(currentPageNum, { width: sw, height: sh, aspectRatio: ar });
applyGlobalAspectRatio(defaultAspectRatio);
return;
}
}
if (curImg.naturalWidth > 0 && curImg.naturalHeight > 0) {
const ar = curImg.naturalWidth / curImg.naturalHeight;
if (ar >= 0.3 && ar <= 2.5) {
defaultAspectRatio = ar > 1.05 ? (ar / 2) : ar;
pageDimensions.set(currentPageNum, { width: curImg.naturalWidth, height: curImg.naturalHeight, aspectRatio: ar });
applyGlobalAspectRatio(defaultAspectRatio);
return;
}
}
}
const infoText = (document.getElementById('i2')?.textContent || '') + ' ' + (document.getElementById('i4')?.textContent || '') + ' ' + (document.getElementById('i1')?.textContent || '');
const mDim = infoText.match(/(\d{3,5})\s*x\s*(\d{3,5})/);
if (mDim) {
const w = parseInt(mDim[1], 10);
const h = parseInt(mDim[2], 10);
if (w > 0 && h > 0) {
const ar = w / h;
if (ar >= 0.3 && ar <= 2.5) {
defaultAspectRatio = ar > 1.05 ? (ar / 2) : ar;
pageDimensions.set(currentPageNum, { width: w, height: h, aspectRatio: ar });
applyGlobalAspectRatio(defaultAspectRatio);
}
}
}
}
function updatePageDimensions(pageNo, width, height) {
if (!width || !height) return;
const ar = width / height;
const prevDim = pageDimensions.get(pageNo);
if (prevDim && prevDim.aspectRatio && Math.abs(prevDim.aspectRatio - ar) < 0.008) {
if (!prevDim.width && width) {
prevDim.width = width;
prevDim.height = height;
}
return;
}
withScrollAnchor(() => {
pageDimensions.set(pageNo, { width, height, aspectRatio: ar });
if (Math.abs(defaultAspectRatio - 0.707) < 0.001 && ar >= 0.4 && ar <= 1.0) {
defaultAspectRatio = ar;
}
const item = pageItemMap.get(pageNo);
if (item) {
applyItemDimensions(item, pageNo);
}
}, pageNo);
if (currentPageNum === pageNo) {
updateTopBarInfo(pageNo);
}
}
function extractKeysFromText(text) {
if (!text) return [];
const found = new Set();
for (const m of text.matchAll(/load_image\((\d+),\s*['"]([^'"]+)['"]\)/g)) {
const p = parseInt(m[1], 10);
pageImgKeyCache.set(p, m[2]);
found.add(p);
}
for (const m of text.matchAll(/\/s\/([0-9a-zA-Z]+)\/\d+-(\d+)/g)) {
const p = parseInt(m[2], 10);
pageImgKeyCache.set(p, m[1]);
found.add(p);
}
return Array.from(found);
}
function extractKeysFromHtml(html) {
if (!html) return;
try {
const template = document.createElement('template');
template.innerHTML = html;
const links = template.content.querySelectorAll('a');
for (const a of links) {
const oc = a.getAttribute('onclick') || '';
const mClick = oc.match(/load_image\((\d+),\s*['"]([^'"]+)['"]\)/i);
if (mClick) {
pageImgKeyCache.set(parseInt(mClick[1], 10), mClick[2]);
}
const href = a.getAttribute('href') || a.href || '';
const mHref = href.match(/\/s\/([0-9a-zA-Z]+)\/\d+-(\d+)/i);
if (mHref) {
pageImgKeyCache.set(parseInt(mHref[2], 10), mHref[1]);
}
}
} catch {}
}
function extractKeysFromResp(resp, pageNo = null) {
if (!resp) return;
const htmlParts = [];
if (resp.i2) htmlParts.push(resp.i2);
if (resp.i4) htmlParts.push(resp.i4);
if (resp.n) htmlParts.push(resp.n);
const combined = htmlParts.join(' ');
extractKeysFromText(combined);
extractKeysFromHtml(combined);
const targetP = pageNo || (resp.p ? parseInt(resp.p, 10) : null);
if (targetP) {
const nlCode = extractNlCode(resp);
if (nlCode) {
pageNlCodes.set(targetP, nlCode);
}
let w = resp.x ? parseInt(resp.x, 10) : 0;
let h = resp.y ? parseInt(resp.y, 10) : 0;
if ((!w || !h) && resp.i3) {
const mDim = resp.i3.match(/style=["'][^"']*width:\s*(\d+)px;\s*height:\s*(\d+)px/i) ||
resp.i3.match(/style=["'][^"']*height:\s*(\d+)px;\s*width:\s*(\d+)px/i);
if (mDim) {
w = parseInt(mDim[1], 10);
h = parseInt(mDim[2], 10);
}
}
if (w > 0 && h > 0) {
updatePageDimensions(targetP, w, h);
}
}
if (!cachedGalleryUrl) {
const combinedAll = (resp.i1 || '') + ' ' + (resp.i2 || '');
const mG = combinedAll.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
if (mG) {
cachedGalleryUrl = `${location.origin}/g/${mG[1]}/${mG[2]}/`;
discoverAndFetchGallery(currentPageNum);
prefetchNearbyThumbnails(currentPageNum);
}
}
}
function extractKeysFromDoc(doc) {
const html =
(doc.getElementById('i2')?.innerHTML || '') + ' ' +
(doc.getElementById('i4')?.innerHTML || '') + ' ' +
(doc.getElementById('i1')?.innerHTML || '');
extractKeysFromText(html);
extractKeysFromHtml(html);
const nl = extractInitialNlCode(doc);
if (nl) {
pageNlCodes.set(currentPageNum, nl);
}
}
function cleanImageInfo(rawStr) {
if (!rawStr) return '';
const text = rawStr.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
if (text.includes('::')) {
const parts = text.split('::').map(s => s.trim());
if (parts.length >= 2) {
let fn = parts[0].replace(/^Page\s+\d+[:_\-]\s*/i, '').trim();
fn = fn.replace(/.*?(?:Download|source)\s+(?:original\s+)?(?:\d+\s*x\s*\d+\s+)?(?:[\d\.]+\s*[kKMmGg]?[iI]?[bB]+\s+)?/i, '').trim();
const dims = parts[1].trim();
const sz = parts[2] ? ` :: ${parts[2].split(/\s+(?:Download|source)/i)[0].trim()}` : '';
return fn ? `${fn} :: ${dims}${sz}` : `${dims}${sz}`;
}
}
const m = text.match(/([^\s<>]+\.[a-zA-Z0-9]+)\s*::\s*(\d+\s*x\s*\d+)(?:\s*::\s*([\d\.]+\s*[kKMmGg]?[iI]?[bB]?))?/i);
if (m) {
const fn = m[1].trim();
const dims = m[2].trim();
const sz = m[3] ? ` :: ${m[3].trim()}` : '';
return fn ? `${fn} :: ${dims}${sz}` : `${dims}${sz}`;
}
return text.replace(/^Page\s+\d+[:_\-]\s*/i, '').trim();
}
function extractInfoFromResp(resp, pageNo = null) {
if (!resp) return '';
// E-Hentai returns the info string in resp.i
const candidates = [resp.i, resp.d, resp.i5, resp.i2, resp.i4, resp.i1, resp.i3];
for (const html of candidates) {
if (!html) continue;
const cleaned = cleanImageInfo(html);
if (cleaned && cleaned.includes('::') && cleaned.includes('x')) {
if (pageNo) {
const sz = parseFileSizeInBytes(cleaned);
if (sz > 0) pageFileSizeCache.set(pageNo, sz);
}
return cleaned;
}
}
let filename = '';
let dimStr = '';
if (resp.i3) {
const mImg = resp.i3.match(/<img[^>]+src=["']([^"']+)["']/i);
const imgSrc = mImg ? mImg[1] : resp.i3;
const mSrc = imgSrc.match(/\/([^\s"'/]+\.[a-zA-Z0-9]+)(?:["']|\?)/i);
if (mSrc && !mSrc[1].includes('blank') && !mSrc[1].includes('loadfail') && !mSrc[1].includes('fullimg')) {
filename = mSrc[1];
}
const mDim = resp.i3.match(/style=["'][^"']*width:\s*(\d+)px;\s*height:\s*(\d+)px/i) ||
resp.i3.match(/style=["'][^"']*height:\s*(\d+)px;\s*width:\s*(\d+)px/i) ||
resp.i3.match(/(\d{3,5})\s*x\s*(\d{3,5})/);
if (mDim) {
dimStr = `${mDim[1]} x ${mDim[2]}`;
}
}
if (resp.x && resp.y) {
dimStr = `${resp.x} x ${resp.y}`;
}
if (!dimStr && pageNo && pageDimensions.has(pageNo)) {
const d = pageDimensions.get(pageNo);
if (d.width && d.height) {
dimStr = `${d.width} x ${d.height}`;
}
}
if (filename && dimStr) return `${filename} :: ${dimStr}`;
if (dimStr) return dimStr;
return filename;
}
function getOrCreateTopBarInfo() {
if (!stripActive) return null;
const i2 = document.getElementById('i2');
if (!i2) return null;
let infoContainer = document.getElementById('eh-topbar-info');
if (!infoContainer) {
infoContainer = document.createElement('div');
infoContainer.id = 'eh-topbar-info';
infoContainer.className = 'eh-topbar-info';
let initialRaw = '';
const dEl = i2.querySelector('.d');
if (dEl) {
initialRaw = dEl.textContent;
dEl.remove();
}
for (const child of Array.from(i2.childNodes)) {
if (child.nodeType === Node.ELEMENT_NODE) {
if (child.classList.contains('sn') || child.id === 'eh-strip-controls' || child.id === 'eh-topbar-info') continue;
if (!initialRaw) initialRaw += ' ' + child.textContent;
child.remove();
} else if (child.nodeType === Node.TEXT_NODE && child.textContent.trim()) {
if (!initialRaw) initialRaw += ' ' + child.textContent;
child.remove();
}
}
const controls = document.getElementById('eh-strip-controls');
if (controls) {
i2.insertBefore(infoContainer, controls);
} else {
i2.appendChild(infoContainer);
}
const cleaned = cleanImageInfo(initialRaw);
if (cleaned) {
pageTopBarInfoCache.set(currentPageNum, cleaned);
infoContainer.innerHTML = `<div>${cleaned}</div>`;
}
}
return infoContainer;
}
function updateTopBarInfo(pageNo) {
const infoContainer = getOrCreateTopBarInfo();
if (!infoContainer) return;
// 1. Inspect live image in DOM
const item = pageItemMap.get(pageNo);
const img = item?.querySelector('img.eh-main-img');
let currentW = 0, currentH = 0;
if (img && img.naturalWidth > 0 && img.naturalHeight > 0) {
currentW = img.naturalWidth;
currentH = img.naturalHeight;
if (!pageDimensions.has(pageNo) || pageDimensions.get(pageNo).width === 0) {
updatePageDimensions(pageNo, currentW, currentH);
}
}
// 2. Inspect pageDimensions cache
if (!currentW || !currentH) {
const dim = pageDimensions.get(pageNo);
if (dim && dim.width > 0 && dim.height > 0) {
currentW = dim.width;
currentH = dim.height;
}
}
// 3. Inspect pageRespCache
let respInfo = '';
if (pageRespCache.has(pageNo)) {
const resp = pageRespCache.get(pageNo);
respInfo = extractInfoFromResp(resp, pageNo);
if (respInfo && respInfo.includes('x') && !currentW) {
const m = respInfo.match(/(\d{2,5})\s*x\s*(\d{2,5})/);
if (m) {
currentW = parseInt(m[1], 10);
currentH = parseInt(m[2], 10);
}
}
}
// 4. Inspect cached info string
let cachedStr = pageTopBarInfoCache.get(pageNo) || '';
if (cachedStr && cachedStr.includes('x') && !currentW) {
const m = cachedStr.match(/(\d{2,5})\s*x\s*(\d{2,5})/);
if (m) {
currentW = parseInt(m[1], 10);
currentH = parseInt(m[2], 10);
}
}
// 5. Render (throttled to avoid layout thrashing during scroll)
let finalStr = '';
if (respInfo && respInfo.includes('::') && respInfo.includes('x')) {
pageTopBarInfoCache.set(pageNo, respInfo);
finalStr = respInfo;
} else if (cachedStr && cachedStr.includes('::') && cachedStr.includes('x')) {
finalStr = cachedStr;
} else {
const baseName = (respInfo && !respInfo.includes('x')) ? respInfo :
(cachedStr && !cachedStr.includes('x')) ? cachedStr : '';
if (baseName && currentW > 0 && currentH > 0) {
finalStr = `${baseName} :: ${currentW} x ${currentH}`;
pageTopBarInfoCache.set(pageNo, finalStr);
} else if (currentW > 0 && currentH > 0) {
finalStr = `Page ${pageNo} :: ${currentW} x ${currentH}`;
} else if (baseName) {
finalStr = baseName;
}
}
if (infoContainer._lastText !== finalStr) {
infoContainer._lastText = finalStr;
infoContainer.innerHTML = finalStr ? `<div>${finalStr}</div>` : '';
}
}
async function loadAndSliceSprite(spriteUrl, entries) {
if (!spriteUrl) return;
if (!spriteEntriesMap.has(spriteUrl)) {
spriteEntriesMap.set(spriteUrl, []);
}
const allEntries = spriteEntriesMap.get(spriteUrl);
if (entries) {
for (const e of entries) {
if (!allEntries.some(existing => existing.page === e.page)) {
allEntries.push(e);
}
}
}
if (spriteDimensionsCache.has(spriteUrl)) {
const { sheetW, sheetH } = spriteDimensionsCache.get(spriteUrl);
for (const e of (entries || allEntries)) {
e.sheetW = sheetW;
e.sheetH = sheetH;
pageThumbnailCache.set(e.page, { type: 'sprite', ...e, sheetW, sheetH });
const item = pageItemMap.get(e.page);
if (item && item.classList.contains('eh-placeholder') && !item.querySelector('img.eh-main-img')) {
applyThumbnailToPlaceholder(item, e.page);
}
if (!stripActive) {
const singleWrap = document.getElementById('eh-single-thumb-placeholder');
if (singleWrap && singleWrap.dataset.page === String(e.page)) {
applyThumbnailToSinglePlaceholder(singleWrap, e.page);
}
}
}
return;
}
if (spriteImagePromiseCache.has(spriteUrl)) {
return spriteImagePromiseCache.get(spriteUrl);
}
const p = (async () => {
try {
const plainImg = new Image();
await new Promise((resolve, reject) => {
plainImg.onload = () => {
plainImg.onload = null;
plainImg.onerror = null;
resolve();
};
plainImg.onerror = (err) => {
plainImg.onload = null;
plainImg.onerror = null;
reject(err);
};
plainImg.src = spriteUrl;
});
if (plainImg.naturalWidth > 0) {
const sheetW = plainImg.naturalWidth;
const sheetH = plainImg.naturalHeight;
plainImg.removeAttribute('src');
plainImg.src = '';
spriteDimensionsCache.set(spriteUrl, { sheetW, sheetH });
for (const e of allEntries) {
e.sheetW = sheetW;
e.sheetH = sheetH;
pageThumbnailCache.set(e.page, { type: 'sprite', ...e, sheetW, sheetH });
const item = pageItemMap.get(e.page);
if (item && item.classList.contains('eh-placeholder') && !item.querySelector('img.eh-main-img')) {
applyThumbnailToPlaceholder(item, e.page);
}
if (!stripActive) {
const singleWrap = document.getElementById('eh-single-thumb-placeholder');
if (singleWrap && singleWrap.dataset.page === String(e.page)) {
applyThumbnailToSinglePlaceholder(singleWrap, e.page);
}
}
}
} else {
plainImg.removeAttribute('src');
plainImg.src = '';
}
} catch (err) {
console.warn('ExFastReader: Failed to load sprite sheet ' + spriteUrl, err);
} finally {
spriteImagePromiseCache.delete(spriteUrl);
}
})();
spriteImagePromiseCache.set(spriteUrl, p);
return p;
}
function createThumbnailElement(info) {
if (!info) return null;
if (info.dataUrl || info.type === 'dataUrl' || info.type === 'url') {
const src = info.url || info.dataUrl;
const img = document.createElement('img');
img.className = 'eh-placeholder-thumb-img';
img.alt = '';
img.src = src;
img.style.cssText = `
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
object-fit: fill !important;
filter: blur(2px) !important;
opacity: 1 !important;
transform: none !important;
pointer-events: none !important;
`;
return img;
} else if (info.type === 'sprite') {
const img = document.createElement('img');
img.className = 'eh-placeholder-thumb-img';
img.alt = '';
const renderSpritePosition = (sw, sh) => {
const estimatedW = (info.h > 150 ? 4000 : 2000);
const sheetW = sw || info.sheetW || Math.max(estimatedW, (info.x || 0) + (info.w || 200));
const sheetH = sh || info.sheetH || (info.h > 150 ? 300 : 140);
const thumbW = info.w || 200;
const thumbH = info.h || 300;
const wRatio = (sheetW / thumbW) * 100;
const hRatio = (sheetH / thumbH) * 100;
const xOffset = ((info.x || 0) / thumbW) * 100;
const yOffset = ((info.y || 0) / thumbH) * 100;
img.style.cssText = `
position: absolute !important;
top: -${yOffset}% !important;
left: -${xOffset}% !important;
width: ${wRatio}% !important;
height: ${hRatio}% !important;
max-width: none !important;
max-height: none !important;
object-fit: fill !important;
filter: blur(2px) !important;
opacity: 1 !important;
transform: none !important;
pointer-events: none !important;
display: block !important;
`;
};
renderSpritePosition();
img.onload = () => {
img.onload = null;
img.onerror = null;
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
info.sheetW = img.naturalWidth;
info.sheetH = img.naturalHeight;
renderSpritePosition(img.naturalWidth, img.naturalHeight);
}
};
img.onerror = () => {
img.onload = null;
img.onerror = null;
};
img.src = info.url;
if (img.complete && img.naturalWidth > 0) {
img.onload = null;
img.onerror = null;
info.sheetW = img.naturalWidth;
info.sheetH = img.naturalHeight;
renderSpritePosition(img.naturalWidth, img.naturalHeight);
}
return img;
}
return null;
}
function applyThumbnailToSinglePlaceholder(wrap, pageNo) {
if (!wrap || wrap.dataset.page !== String(pageNo)) return;
const info = pageThumbnailCache.get(pageNo);
if (info) {
const oldImgs = wrap.querySelectorAll('img');
oldImgs.forEach(i => {
i.onload = null;
i.onerror = null;
i.removeAttribute('src');
i.src = '';
});
wrap.innerHTML = '';
const thumbWrap = document.createElement('div');
thumbWrap.className = 'eh-placeholder-thumb-wrap';
const thumbImg = createThumbnailElement(info);
if (thumbImg) {
thumbWrap.appendChild(thumbImg);
wrap.appendChild(thumbWrap);
}
} else {
if (!wrap.querySelector('.eh-placeholder-content')) {
const oldImgs = wrap.querySelectorAll('img');
oldImgs.forEach(i => {
i.onload = null;
i.onerror = null;
i.removeAttribute('src');
i.src = '';
});
wrap.innerHTML = '';
const content = document.createElement('div');
content.className = 'eh-placeholder-content';
content.innerHTML = `<div class="eh-spinner"></div><div class="eh-placeholder-text">Page ${pageNo}...</div>`;
wrap.appendChild(content);
}
prefetchNearbyThumbnails(pageNo);
}
const pageKey = 'eh_retry_' + location.pathname + '_' + pageNo;
const retryCount = parseInt(safeStorageGet(sessionStorage, pageKey, '0'), 10);
if (retryCount > 0) {
const oldContent = wrap.querySelector('.eh-placeholder-content');
if (oldContent) oldContent.remove();
const content = document.createElement('div');
content.className = 'eh-placeholder-content';
content.innerHTML = `<div class="eh-spinner"></div><div class="eh-placeholder-text">Retrying Page ${pageNo}...</div>`;
wrap.appendChild(content);
}
}
function showSinglePageThumbnailPlaceholder(pageNo) {
if (stripActive) return;
const i3 = document.getElementById('i3');
if (!i3) return;
const imgEl = i3.querySelector('img#img') || i3.querySelector('img');
if (imgEl && imgEl._eh_page === pageNo && !imgEl._eh_obsolete && imgEl.complete && imgEl.naturalWidth > 0) {
imgEl.style.display = 'block';
hideSinglePageThumbnailPlaceholder();
return;
}
if (imgEl && imgEl._eh_page !== pageNo) {
imgEl.style.display = 'none';
imgEl._eh_obsolete = true;
imgEl.__eh_monitored = false;
} else if (imgEl) {
imgEl.style.display = 'none';
}
let wrap = document.getElementById('eh-single-thumb-placeholder');
if (!wrap) {
wrap = document.createElement('div');
wrap.id = 'eh-single-thumb-placeholder';
if (imgEl && imgEl.parentNode) {
imgEl.parentNode.insertBefore(wrap, imgEl);
} else {
i3.appendChild(wrap);
}
}
wrap.dataset.page = String(pageNo);
const dim = pageDimensions.get(pageNo);
let ar = (dim && dim.aspectRatio) ? dim.aspectRatio : defaultAspectRatio;
let targetW = (dim && dim.width > 0) ? dim.width : 0;
let targetH = (dim && dim.height > 0) ? dim.height : 0;
if (!targetW && pageRespCache.has(pageNo)) {
const resp = pageRespCache.get(pageNo);
if (resp && resp.i3) {
const mW = resp.i3.match(/style=["'][^"']*width:\s*(\d+)px/i);
const mH = resp.i3.match(/style=["'][^"']*height:\s*(\d+)px/i);
if (mW && mH) {
targetW = parseInt(mW[1], 10);
targetH = parseInt(mH[1], 10);
ar = targetW / targetH;
}
}
if (!targetW && resp && resp.x && resp.y) {
targetW = parseInt(resp.x, 10);
targetH = parseInt(resp.y, 10);
ar = targetW / targetH;
}
}
if (!targetW) {
const prevImg = document.getElementById('img');
const prevW = prevImg ? (parseInt(prevImg.style.width, 10) || prevImg.naturalWidth) : 0;
targetW = (prevW > 400 && prevW < 3000) ? prevW : Math.min(1280, window.innerWidth - 24);
targetH = Math.round(targetW / ar);
} else if (!targetH) {
targetH = Math.round(targetW / ar);
}
const maxW = Math.min(targetW, window.innerWidth - 24);
const maxH = Math.round(maxW / ar);
wrap.style.cssText = `
position: relative !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin: 0 auto !important;
width: ${maxW}px !important;
max-width: 100% !important;
height: ${maxH}px !important;
aspect-ratio: ${ar} !important;
box-sizing: border-box !important;
overflow: hidden !important;
`;
applyThumbnailToSinglePlaceholder(wrap, pageNo);
}
function hideSinglePageThumbnailPlaceholder() {
const i3 = document.getElementById('i3');
const imgEl = i3 ? (i3.querySelector('img#img') || i3.querySelector('img')) : null;
if (imgEl && imgEl._eh_page === currentPageNum && !imgEl._eh_obsolete && imgEl.complete && imgEl.naturalWidth > 0) {
imgEl.style.display = 'block';
}
const wrap = document.getElementById('eh-single-thumb-placeholder');
if (wrap) {
const oldImgs = wrap.querySelectorAll('img');
oldImgs.forEach(i => {
i.onload = null;
i.onerror = null;
i.removeAttribute('src');
i.src = '';
});
wrap.remove();
}
}
function applyThumbnailToPlaceholder(item, pageNo) {
if (!item || !item.classList.contains('eh-placeholder')) return;
if (item.querySelector('img.eh-main-img')) return;
const info = pageThumbnailCache.get(pageNo);
if (!info) return;
const existingThumbs = item.querySelectorAll('.eh-placeholder-thumb-img, .eh-placeholder-thumb-wrap');
existingThumbs.forEach(el => {
if (el.tagName === 'IMG') {
el.onload = null;
el.onerror = null;
el.removeAttribute('src');
el.src = '';
} else {
const imgs = el.querySelectorAll('img');
imgs.forEach(i => { i.onload = null; i.onerror = null; i.removeAttribute('src'); i.src = ''; });
}
el.remove();
});
const content = item.querySelector('.eh-placeholder-content');
const wrap = document.createElement('div');
wrap.className = 'eh-placeholder-thumb-wrap';
if (info.dataUrl || info.type === 'dataUrl' || info.type === 'url') {
const src = info.url || info.dataUrl;
const img = document.createElement('img');
img.className = 'eh-placeholder-thumb-img';
img.alt = '';
img.src = src;
img.style.cssText = `
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
object-fit: fill !important;
filter: blur(2px) !important;
opacity: 1 !important;
transform: none !important;
pointer-events: none !important;
`;
wrap.appendChild(img);
} else if (info.type === 'sprite') {
const img = document.createElement('img');
img.className = 'eh-placeholder-thumb-img';
img.alt = '';
const renderSpritePosition = (sw, sh) => {
const estimatedW = (info.h > 150 ? 4000 : 2000);
const sheetW = sw || info.sheetW || Math.max(estimatedW, (info.x || 0) + (info.w || 200));
const sheetH = sh || info.sheetH || (info.h > 150 ? 300 : 140);
const thumbW = info.w || 200;
const thumbH = info.h || 300;
const wRatio = (sheetW / thumbW) * 100;
const hRatio = (sheetH / thumbH) * 100;
const xOffset = ((info.x || 0) / thumbW) * 100;
const yOffset = ((info.y || 0) / thumbH) * 100;
img.style.cssText = `
position: absolute !important;
top: -${yOffset}% !important;
left: -${xOffset}% !important;
width: ${wRatio}% !important;
height: ${hRatio}% !important;
max-width: none !important;
max-height: none !important;
object-fit: fill !important;
filter: blur(2px) !important;
opacity: 1 !important;
transform: none !important;
pointer-events: none !important;
display: block !important;
`;
};
renderSpritePosition();
img.onload = () => {
img.onload = null;
img.onerror = null;
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
info.sheetW = img.naturalWidth;
info.sheetH = img.naturalHeight;
renderSpritePosition(img.naturalWidth, img.naturalHeight);
}
};
img.onerror = () => {
img.onload = null;
img.onerror = null;
};
img.src = info.url;
if (img.complete && img.naturalWidth > 0) {
img.onload = null;
img.onerror = null;
info.sheetW = img.naturalWidth;
info.sheetH = img.naturalHeight;
renderSpritePosition(img.naturalWidth, img.naturalHeight);
}
wrap.appendChild(img);
}
if (content) {
item.insertBefore(wrap, content);
} else {
item.appendChild(wrap);
}
}
async function drainGalleryQueue(queue) {
if (!queue || queue._draining) return;
queue._draining = true;
const worker = async () => {
while (queue.length > 0) {
const pIndex = queue.shift();
await fetchGalleryPage(pIndex);
if (queue.length > 0) {
await new Promise(r => setTimeout(r, 300));
}
}
queue._draining = false;
};
await worker();
}
function prefetchNearbyThumbnails(curPage) {
if (!isReaderPage) return;
const total = getTotalPages();
const THUMB_WINDOW = 20;
const startP = Math.max(1, curPage - 10);
const endP = total > 0 ? Math.min(total, curPage + THUMB_WINDOW) : (curPage + THUMB_WINDOW);
const galleryPagesToFetch = [];
for (let p = startP; p <= endP; p++) {
if (!pageThumbnailCache.has(p) || !pageImgKeyCache.has(p)) {
const gIndex = Math.max(0, Math.floor((p - 1) / thumbsPerPage));
if (!fetchedGalleryPages.has(gIndex) && !inFlightGalleryFetches.has(gIndex)) {
if (!galleryPagesToFetch.includes(gIndex)) {
galleryPagesToFetch.push(gIndex);
}
}
}
}
for (const gIndex of galleryPagesToFetch) {
fetchGalleryPage(gIndex);
}
}
async function discoverAndFetchGallery(targetPage = 1) {
let galleryUrl = getGalleryUrl();
if (!galleryUrl) {
for (let attempt = 0; attempt < 12; attempt++) {
await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
galleryUrl = getGalleryUrl();
if (galleryUrl) break;
}
if (!galleryUrl) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => discoverAndFetchGallery(targetPage), { once: true });
}
return;
}
}
const targetIndex = Math.max(0, Math.floor((targetPage - 1) / thumbsPerPage));
if (pageThumbnailCache.has(targetPage) && pageImgKeyCache.has(targetPage)) {
fetchedGalleryPages.set(targetIndex, Date.now());
} else {
await fetchGalleryPage(targetIndex);
}
const total = getTotalPages();
if (total > 0) {
const totalGalleryPages = Math.ceil(total / thumbsPerPage);
const queue = [];
// Window of 20 gallery pages for deep early thumbnail prefetching
const maxOffset = Math.min(2, totalGalleryPages);
for (let offset = 1; offset <= maxOffset; offset++) {
const f = targetIndex + offset;
const b = targetIndex - offset;
if (f < totalGalleryPages && !fetchedGalleryPages.has(f)) queue.push(f);
if (b >= 0 && !fetchedGalleryPages.has(b)) queue.push(b);
}
drainGalleryQueue(queue);
}
}
function extractDimensionsFromGallery(source) {
if (!source) return 0;
let count = 0;
try {
let doc;
let htmlStr = '';
if (typeof source === 'string') {
htmlStr = source;
doc = new DOMParser().parseFromString(source, 'text/html');
} else if (source && (source.nodeType || source.querySelector)) {
doc = source;
htmlStr = (source.documentElement ? source.documentElement.innerHTML : source.innerHTML) || '';
}
if (!doc) return 0;
const mGpc = htmlStr.match(/Showing\s+(\d+)\s*-\s*(\d+)\s+of\s+([\d,]+)\s+images/i) ||
(doc.querySelector('.gpc, #gpc')?.textContent || '').match(/Showing\s+(\d+)\s*-\s*(\d+)\s+of\s+([\d,]+)\s+images/i);
if (mGpc) {
const sNum = parseInt(mGpc[1], 10);
const eNum = parseInt(mGpc[2], 10);
const tot = parseInt(mGpc[3].replace(/,/g, ''), 10);
if (eNum < tot) {
const countPerPg = eNum - sNum + 1;
if ([20, 40, 100].includes(countPerPg)) {
thumbsPerPage = countPerPg;
safeStorageSet(localStorage, 'eh_thumbs_per_page', String(countPerPg));
}
}
if (tot > 0 && (!cachedTotalPages || cachedTotalPages < tot)) {
cachedTotalPages = tot;
if (isReaderPage && stripActive) {
ensureAllPlaceholders(tot);
}
}
}
const links = doc.querySelectorAll('a[href*="/s/"]');
const spritesToSlice = new Map();
withScrollAnchor(() => {
for (const a of links) {
const href = a.getAttribute('href') || a.href || '';
const m = href.match(/\/s\/([0-9a-zA-Z]+)\/\d+-(\d+)/i);
if (!m) continue;
count++;
const key = m[1];
const page = parseInt(m[2], 10);
pageImgKeyCache.set(page, key);
const img = a.querySelector('img');
const thumbDiv = a.querySelector('[style*="url("]') ||
a.querySelector('[style*="background"]') ||
a.closest('[style*="url("]') ||
a.closest('[style*="background"]') ||
a.querySelector('div[title]') ||
a.querySelector('div');
const parent = a.closest('.gdtm > div') || a.closest('.gdtm') || a.closest('.gdtl') || a.parentElement;
const titleEl = a.querySelector('[title*="Page"]') ||
a.querySelector('[title]') ||
thumbDiv ||
img ||
a ||
parent;
const title = (
titleEl?.getAttribute('title') ||
img?.getAttribute('title') ||
a.getAttribute('title') ||
parent?.getAttribute('title') ||
img?.getAttribute('alt') ||
''
).trim();
const cleanInfo = cleanImageInfo(title);
if (cleanInfo) {
const existingInfo = pageTopBarInfoCache.get(page) || '';
if (!existingInfo || !existingInfo.includes('x') || cleanInfo.includes('x')) {
pageTopBarInfoCache.set(page, cleanInfo);
}
const sz = parseFileSizeInBytes(cleanInfo);
if (sz > 0) pageFileSizeCache.set(page, sz);
if (currentPageNum === page) {
updateTopBarInfo(page);
}
}
const styleStr = [
thumbDiv?.getAttribute('style') || '',
a.getAttribute('style') || '',
parent?.getAttribute('style') || '',
img?.getAttribute('style') || ''
].join(' ');
let w = 0, h = 0;
const mW = styleStr.match(/(?:^|[;\s])width:\s*(\d+)px/i);
const mH = styleStr.match(/(?:^|[;\s])height:\s*(\d+)px/i);
if (mW) w = parseInt(mW[1], 10);
if (mH) h = parseInt(mH[1], 10);
if (!w && img) {
w = parseInt(img.style.width || img.getAttribute('width') || '0', 10);
}
if (!h && img) {
h = parseInt(img.style.height || img.getAttribute('height') || '0', 10);
}
if (!w && thumbDiv) {
w = parseInt(thumbDiv.style.width || thumbDiv.getAttribute('width') || '0', 10);
}
if (!h && thumbDiv) {
h = parseInt(thumbDiv.style.height || thumbDiv.getAttribute('height') || '0', 10);
}
const dimMatch = (cleanInfo || title).match(/(\d{3,5})\s*x\s*(\d{3,5})/);
let hasRealDim = false;
if (dimMatch) {
const realW = parseInt(dimMatch[1], 10);
const realH = parseInt(dimMatch[2], 10);
if (realW > 0 && realH > 0) {
const ar = realW / realH;
pageDimensions.set(page, { width: realW, height: realH, aspectRatio: ar });
const item = pageItemMap.get(page);
if (item) applyItemDimensions(item, page);
hasRealDim = true;
}
}
if (!hasRealDim && w > 0 && h > 0) {
const ar = w / h;
if (ar >= 0.3 && ar <= 2.8) {
if (!pageDimensions.has(page) || pageDimensions.get(page).width === 0) {
pageDimensions.set(page, { width: 0, height: 0, aspectRatio: ar });
const item = pageItemMap.get(page);
if (item) applyItemDimensions(item, page);
}
}
}
const imgSrc = img ? (img.getAttribute('src') || img.src || '') : '';
const isBlank = !imgSrc || imgSrc.includes('blank.gif');
const mUrl = styleStr.match(/url\(["']?([^"')]+)["']?\)/i);
const bgUrl = mUrl ? mUrl[1].trim() : '';
if (img && !isBlank) {
pageThumbnailCache.set(page, { type: 'url', url: imgSrc, w, h });
const preImg = new Image();
preImg.decoding = 'async';
preImg.src = imgSrc;
const item = pageItemMap.get(page);
if (item && item.classList.contains('eh-placeholder') && !item.querySelector('img.eh-main-img')) {
applyThumbnailToPlaceholder(item, page);
}
if (!stripActive) {
const singleWrap = document.getElementById('eh-single-thumb-placeholder');
if (singleWrap && singleWrap.dataset.page === String(page)) {
applyThumbnailToSinglePlaceholder(singleWrap, page);
}
}
} else if (bgUrl) {
let posX = 0, posY = 0;
const mPos = styleStr.match(/background-position:\s*(-?\d+(?:px)?)\s+(-?\d+(?:px)?)/i) ||
styleStr.match(/url\([^)]+\)\s+(-?\d+(?:px)?)\s+(-?\d+(?:px)?)/i) ||
styleStr.match(/(-?\d+(?:px)?)\s+(-?\d+(?:px)?)\s+no-repeat/i) ||
styleStr.match(/no-repeat\s+(-?\d+(?:px)?)\s+(-?\d+(?:px)?)/i) ||
styleStr.match(/(-?\d+px)\s+(-?\d+px)/i);
if (mPos) {
posX = Math.abs(parseInt(mPos[1].replace(/[^\d-]/g, ''), 10) || 0);
posY = Math.abs(parseInt(mPos[2].replace(/[^\d-]/g, ''), 10) || 0);
}
const thumbW = w || 200;
const thumbH = h || 300;
let sheetW = 0, sheetH = 0;
if (spriteDimensionsCache.has(bgUrl)) {
const sDim = spriteDimensionsCache.get(bgUrl);
sheetW = sDim.sheetW;
sheetH = sDim.sheetH;
}
const entry = { page, x: posX, y: posY, w: thumbW, h: thumbH, url: bgUrl, sheetW, sheetH };
pageThumbnailCache.set(page, { type: 'sprite', ...entry });
const preSprite = new Image();
preSprite.decoding = 'async';
preSprite.src = bgUrl;
if (sheetW > 0 && sheetH > 0) {
const item = pageItemMap.get(page);
if (item && item.classList.contains('eh-placeholder') && !item.querySelector('img.eh-main-img')) {
applyThumbnailToPlaceholder(item, page);
}
if (!stripActive && currentPageNum === page) {
showSinglePageThumbnailPlaceholder(page);
}
}
if (!spritesToSlice.has(bgUrl)) {
spritesToSlice.set(bgUrl, []);
}
spritesToSlice.get(bgUrl).push(entry);
}
}
});
for (const [bgUrl, entries] of spritesToSlice.entries()) {
const maxW = Math.max(...entries.map(e => e.x + (e.w || 200)));
const maxH = Math.max(...entries.map(e => e.y + (e.h || 300)));
const estimatedSheetW = Math.max(2000, Math.ceil(maxW / 100) * 100);
const estimatedSheetH = Math.max(maxH > 150 ? 300 : 140, maxH);
for (const e of entries) {
if (!e.sheetW) {
e.sheetW = estimatedSheetW;
e.sheetH = estimatedSheetH;
pageThumbnailCache.set(e.page, { type: 'sprite', ...e, sheetW: estimatedSheetW, sheetH: estimatedSheetH });
const item = pageItemMap.get(e.page);
if (item && item.classList.contains('eh-placeholder') && !item.querySelector('img.eh-main-img')) {
applyThumbnailToPlaceholder(item, e.page);
}
if (!stripActive && currentPageNum === e.page) {
showSinglePageThumbnailPlaceholder(e.page);
}
}
}
loadAndSliceSprite(bgUrl, entries);
}
if (cachedGid) {
saveGalleryDataToSession(cachedGid);
}
} catch (err) {
console.warn('ExFastReader: Failed to extract gallery dimensions/thumbnails', err);
}
return count;
}
async function fetchGalleryPage(pageIndex) {
if (pageIndex < 0) return;
const lastFetch = fetchedGalleryPages.get(pageIndex);
if (lastFetch && Date.now() - lastFetch < 30000) {
return;
}
if (inFlightGalleryFetches.has(pageIndex)) {
return inFlightGalleryFetches.get(pageIndex);
}
const baseGalleryUrl = getBaseGalleryUrl();
if (!baseGalleryUrl) return;
const promise = (async () => {
try {
let fetchUrl;
try {
const u = new URL(baseGalleryUrl, location.origin);
if (pageIndex > 0) {
u.searchParams.set('p', String(pageIndex));
} else {
u.searchParams.delete('p');
}
fetchUrl = u.href;
} catch {
const cleanBase = baseGalleryUrl.replace(/[?&]p=\d+/g, '').replace(/\?$/, '');
fetchUrl = pageIndex > 0 ? (cleanBase + (cleanBase.includes('?') ? '&' : '?') + 'p=' + pageIndex) : cleanBase;
}
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
const timer = setTimeout(() => { if (controller) controller.abort(); }, 12000);
const res = await fetch(fetchUrl, {
credentials: 'include',
signal: controller ? controller.signal : undefined
}).finally(() => clearTimeout(timer));
if (res.ok) {
fetchedGalleryPages.set(pageIndex, Date.now());
const html = await res.text();
extractKeysFromText(html);
extractDimensionsFromGallery(html);
} else {
fetchedGalleryPages.set(pageIndex, Date.now() - 25000);
}
} catch (err) {
console.warn('ExFastReader: Gallery fetch failed for p=' + pageIndex, err);
fetchedGalleryPages.set(pageIndex, Date.now() - 25000);
} finally {
inFlightGalleryFetches.delete(pageIndex);
}
})();
inFlightGalleryFetches.set(pageIndex, promise);
return promise;
}
async function ensureKeysForPage(targetPageNo) {
if (pageImgKeyCache.has(targetPageNo)) {
return pageImgKeyCache.get(targetPageNo);
}
const total = getTotalPages();
if (total > 0 && targetPageNo > total) {
return null;
}
const primaryIndex = Math.max(0, Math.floor((targetPageNo - 1) / thumbsPerPage));
await fetchGalleryPage(primaryIndex);
if (pageImgKeyCache.has(targetPageNo)) {
return pageImgKeyCache.get(targetPageNo);
}
for (const altCount of [20, 40, 100]) {
if (altCount === thumbsPerPage) continue;
const altIndex = Math.max(0, Math.floor((targetPageNo - 1) / altCount));
if (altIndex !== primaryIndex) {
await fetchGalleryPage(altIndex);
if (pageImgKeyCache.has(targetPageNo)) {
thumbsPerPage = altCount;
return pageImgKeyCache.get(targetPageNo);
}
}
}
return pageImgKeyCache.get(targetPageNo) || null;
}
function getNextMeta(doc) {
const nextEl = doc.querySelector('a#next');
if (!nextEl) return null;
const oc = nextEl.getAttribute('onclick') || '';
const mClick = oc.match(/load_image\((\d+),\s*['"]([^'"]+)['"]\)/i);
if (mClick) return { page: parseInt(mClick[1], 10), imgKey: mClick[2] };
const mHref = (nextEl.getAttribute('href') || nextEl.href || '').match(/\/s\/([0-9a-zA-Z]+)\/\d+-(\d+)/i);
if (mHref) return { page: parseInt(mHref[2], 10), imgKey: mHref[1] };
return null;
}
function getTotalPages() {
if (cachedTotalPages > 0) return cachedTotalPages;
const spans = document.querySelectorAll('#i2 .sn span, #i4 .sn span');
for (let i = 0; i < spans.length; i++) {
const t = parseInt(spans[i].textContent.trim(), 10);
if (!isNaN(t) && t > 1) {
if (i + 1 < spans.length) {
const nextT = parseInt(spans[i + 1].textContent.trim(), 10);
if (!isNaN(nextT) && nextT >= t) {
cachedTotalPages = nextT;
return nextT;
}
}
cachedTotalPages = t;
return t;
}
}
const snEl = document.querySelector('#i2 .sn, #i4 .sn, .sn');
if (snEl) {
const match = snEl.textContent.match(/(\d+)\s*\/\s*(\d+)/);
if (match) {
const total = parseInt(match[2], 10);
if (!isNaN(total) && total > 0) {
cachedTotalPages = total;
return total;
}
}
}
return 0;
}
function getPageParams() {
if (cachedGid && cachedShowkey) {
return { gid: cachedGid, showkey: cachedShowkey, api_url: cachedApiUrl };
}
let gid = window.gid || cachedGid;
let showkey = window.showkey || cachedShowkey;
let api_url = window.api_url || cachedApiUrl;
if (!gid || !showkey) {
for (const s of document.scripts) {
const txt = s.textContent || '';
if (!gid) {
const mG = txt.match(/var\s+gid\s*=\s*(\d+)/);
if (mG) gid = parseInt(mG[1], 10);
}
if (!showkey) {
const mS = txt.match(/var\s+showkey\s*=\s*['"]([^'"]+)['"]/);
if (mS) showkey = mS[1];
}
if (!api_url) {
const mA = txt.match(/var\s+api_url\s*=\s*['"]([^'"]+)['"]/);
if (mA) api_url = mA[1];
}
if (gid && showkey && api_url) break;
}
}
if (gid) cachedGid = parseInt(gid, 10);
if (showkey) cachedShowkey = showkey;
if (api_url) cachedApiUrl = normalizeApiUrl(api_url);
else cachedApiUrl = location.origin + '/api.php';
return { gid: cachedGid || 0, showkey, api_url: cachedApiUrl };
}
function pruneCaches(activePage) {
if (stripActive) {
withScrollAnchor(() => {
const UNLOAD_DISTANCE = 15;
for (const p of Array.from(loadedImages)) {
if (Math.abs(p - activePage) > UNLOAD_DISTANCE) {
const item = pageItemMap.get(p);
if (item) {
if (typeof item._abortImg === 'function') {
item._abortImg();
item._abortImg = null;
}
const img = item.querySelector('img.eh-main-img');
if (img) {
img.onload = null;
img.onerror = null;
img.removeAttribute('src');
img.src = '';
img.remove();
}
item._currentImgToken = null;
loadedImages.delete(p);
renderPlaceholderContent(item, p);
}
}
}
// Also abort distant in-flight image loads to free connection pool
for (const [p, startTime] of loadingPages.entries()) {
if (Math.abs(p - activePage) > UNLOAD_DISTANCE) {
loadingPages.delete(p);
const item = pageItemMap.get(p);
if (item) {
item.classList.remove('eh-loading');
if (typeof item._abortImg === 'function') {
item._abortImg();
item._abortImg = null;
} else if (item._currentImgToken) {
const img = item._currentImgToken;
img.onload = null;
img.onerror = null;
img.removeAttribute('src');
img.src = '';
}
item._currentImgToken = null;
}
}
}
// Unload distant placeholder thumbnail images (> 30 pages away) to free memory
const THUMB_UNLOAD_DISTANCE = 30;
for (const [p, item] of pageItemMap.entries()) {
if (Math.abs(p - activePage) > THUMB_UNLOAD_DISTANCE && item.classList.contains('eh-placeholder')) {
const oldThumbs = item.querySelectorAll('.eh-placeholder-thumb-img, .eh-placeholder-thumb-wrap');
if (oldThumbs.length > 0) {
oldThumbs.forEach(t => {
if (t.tagName === 'IMG') {
t.onload = null;
t.onerror = null;
t.removeAttribute('src');
t.src = '';
} else {
t.querySelectorAll('img').forEach(i => {
i.onload = null;
i.onerror = null;
i.removeAttribute('src');
i.src = '';
});
}
t.remove();
});
}
}
}
});
}
for (const [page, record] of preloadedImages.entries()) {
if (stripActive || page < activePage - 2 || page > activePage + 3) {
if (record && record.img) {
record.img.onload = null;
record.img.onerror = null;
record.img.removeAttribute('src');
record.img.src = '';
}
preloadedImages.delete(page);
}
}
const respLimit = stripActive ? 80 : 10;
if (pageRespCache.size > respLimit) {
for (const page of pageRespCache.keys()) {
if (Math.abs(page - activePage) > (stripActive ? 40 : 3)) {
pageRespCache.delete(page);
if (pageRespCache.size <= respLimit) break;
}
}
}
if (pageImgSrcCache.size > 100) {
for (const page of pageImgSrcCache.keys()) {
if (Math.abs(page - activePage) > 50) {
pageImgSrcCache.delete(page);
if (pageImgSrcCache.size <= 100) break;
}
}
}
for (const key of inFlightFetches.keys()) {
const p = parseInt(key, 10);
if (!isNaN(p) && Math.abs(p - activePage) > 20) {
inFlightFetches.delete(key);
}
}
if (pageTopBarInfoCache.size > 150) {
for (const page of pageTopBarInfoCache.keys()) {
if (Math.abs(page - activePage) > 75) {
pageTopBarInfoCache.delete(page);
if (pageTopBarInfoCache.size <= 150) break;
}
}
}
if (pageFileSizeCache.size > 150) {
for (const page of pageFileSizeCache.keys()) {
if (Math.abs(page - activePage) > 75) {
pageFileSizeCache.delete(page);
if (pageFileSizeCache.size <= 150) break;
}
}
}
if (pageNlCodes.size > 80) {
for (const page of pageNlCodes.keys()) {
if (Math.abs(page - activePage) > 40) {
pageNlCodes.delete(page);
if (pageNlCodes.size <= 80) break;
}
}
}
if (failedPages.size > 50) {
for (const page of failedPages.keys()) {
if (Math.abs(page - activePage) > 40) {
failedPages.delete(page);
if (failedPages.size <= 50) break;
}
}
}
const curGalleryIdx = Math.max(0, Math.floor((activePage - 1) / thumbsPerPage));
if (fetchedGalleryPages.size > 40) {
for (const gIdx of fetchedGalleryPages.keys()) {
if (Math.abs(gIdx - curGalleryIdx) > 15) {
fetchedGalleryPages.delete(gIdx);
if (fetchedGalleryPages.size <= 40) break;
}
}
}
if (pageImgKeyCache.size > 500) {
for (const page of pageImgKeyCache.keys()) {
if (Math.abs(page - activePage) > 250) {
pageImgKeyCache.delete(page);
if (pageImgKeyCache.size <= 500) break;
}
}
}
}
function fetchApi(pageNo, imgKey, nlCode = null, forceFresh = false, isPriority = false) {
const parsedPage = parseInt(pageNo, 10);
const tot = getTotalPages();
if (!parsedPage || isNaN(parsedPage) || parsedPage < 1 || (tot > 0 && parsedPage > tot)) {
return Promise.resolve(null);
}
if (!imgKey || typeof imgKey !== 'string' || !/^[0-9a-zA-Z]+$/.test(imgKey)) {
return Promise.resolve(null);
}
const cacheKey = parsedPage + (nlCode ? '_' + nlCode : '');
if (!forceFresh && inFlightFetches.has(cacheKey)) {
return inFlightFetches.get(cacheKey);
}
const { gid, showkey } = getPageParams();
if (!gid || !showkey || !imgKey) return Promise.resolve(null);
const requestPayload = {
method: 'showpage',
gid: parseInt(gid, 10),
page: parsedPage,
imgkey: String(imgKey).trim(),
showkey: showkey
};
if (nlCode) {
requestPayload.nl = nlCode;
}
const fetchPromise = (async () => {
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
// Increased from 10s to 18s to allow queued background requests to complete
const timer = setTimeout(() => {
if (controller) controller.abort();
}, 18000);
try {
// ALWAYS send to same-origin /api.php
const res = await fetch(location.origin + '/api.php', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(requestPayload),
signal: controller ? controller.signal : undefined,
priority: isPriority ? 'high' : 'low'
});
clearTimeout(timer);
if (!res.ok) {
console.warn(`[ExFastReader] API returned HTTP ${res.status} on page ${parsedPage}`);
return null;
}
const text = await res.text();
const data = safeParseJson(text);
if (data && !data.error) {
return data;
} else {
if (data && data.error) {
console.warn(`[ExFastReader] API returned error on page ${parsedPage}:`, data.error);
}
return null;
}
} catch (err) {
clearTimeout(timer);
console.warn(`[ExFastReader] Request failed on page ${parsedPage}:`, err.name === 'AbortError' ? 'Timed out (18s)' : err.message);
return null;
} finally {
inFlightFetches.delete(cacheKey);
}
})();
inFlightFetches.set(cacheKey, fetchPromise);
return fetchPromise;
}
function prefetchPage(pageNo, depthRemaining) {
if (stripActive) return;
const onResponse = (resp) => {
pageRespCache.set(pageNo, resp);
const extracted = extractInfoFromResp(resp, pageNo);
if (extracted) pageTopBarInfoCache.set(pageNo, extracted);
const nextDoc = parseTemplate(resp.n);
const nextMeta = getNextMeta(nextDoc);
if (nextMeta) {
pageImgKeyCache.set(nextMeta.page, nextMeta.imgKey);
}
const i3Doc = parseTemplate(resp.i3);
const imgEl = i3Doc.querySelector('img#img');
const imgSrc = imgEl ? (imgEl.getAttribute('src') || imgEl.src) : null;
if (depthRemaining > 0 && nextMeta) {
prefetchPage(nextMeta.page, depthRemaining - 1);
}
if (imgSrc && !preloadedImages.has(pageNo)) {
const img = new Image();
img.fetchPriority = 'low';
img.decoding = 'async';
const record = { img, isReady: false, promise: null };
record.promise = new Promise((resolve) => {
img.onload = async () => {
img.onload = null;
img.onerror = null;
try {
if ('decode' in img) await img.decode();
} catch {}
record.isReady = true;
resolve(img);
};
img.onerror = () => {
img.onload = null;
img.onerror = null;
record.isReady = false;
resolve(null);
};
img.src = imgSrc;
});
preloadedImages.set(pageNo, record);
}
};
if (pageRespCache.has(pageNo)) {
onResponse(pageRespCache.get(pageNo));
return;
}
const imgKey = pageImgKeyCache.get(pageNo);
if (!imgKey) return;
fetchApi(pageNo, imgKey).then((resp) => {
if (resp) onResponse(resp);
});
}
function prefetchSingleMode(cur) {
if (stripActive) return;
const total = getTotalPages();
prefetchNearbyThumbnails(cur);
const targets = [cur + 1, cur + 2, cur + 3, cur - 1].filter(p => p >= 1 && (!total || p <= total));
for (const p of targets) {
if (pageRespCache.has(p)) {
const resp = pageRespCache.get(p);
const i3Doc = parseTemplate(resp.i3);
const imgEl = i3Doc.querySelector('img#img') || i3Doc.querySelector('img');
const imgSrc = imgEl ? (imgEl.getAttribute('src') || imgEl.src) : null;
if (imgSrc && !preloadedImages.has(p)) {
preloadSingleImage(p, imgSrc);
}
continue;
}
const imgKey = pageImgKeyCache.get(p);
if (imgKey) {
fetchApi(p, imgKey).then((resp) => {
if (resp) {
pageRespCache.set(p, resp);
extractKeysFromResp(resp, p);
const extracted = extractInfoFromResp(resp, p);
if (extracted) pageTopBarInfoCache.set(p, extracted);
const i3Doc = parseTemplate(resp.i3);
const imgEl = i3Doc.querySelector('img#img') || i3Doc.querySelector('img');
const imgSrc = imgEl ? (imgEl.getAttribute('src') || imgEl.src) : null;
if (imgSrc) {
pageImgSrcCache.set(p, imgSrc);
preloadSingleImage(p, imgSrc);
}
}
});
}
}
}
function preloadSingleImage(pageNo, src) {
if (preloadedImages.has(pageNo)) return;
const img = new Image();
img.fetchPriority = 'low';
img.decoding = 'async';
const record = { img, isReady: false, promise: null };
const startTime = Date.now();
record.promise = new Promise((resolve) => {
img.onload = async () => {
img.onload = null;
img.onerror = null;
try {
if ('decode' in img) await img.decode();
} catch {}
record.isReady = true;
const durationSec = Math.max(0.1, (Date.now() - startTime) / 1000);
recordDownloadSpeedSample(pageNo, durationSec);
resolve(img);
};
img.onerror = () => {
img.onload = null;
img.onerror = null;
record.isReady = false;
resolve(null);
};
img.src = src;
});
preloadedImages.set(pageNo, record);
}
function startPrefetch() {
const nextMeta = getNextMeta(document);
if (nextMeta) {
pageImgKeyCache.set(nextMeta.page, nextMeta.imgKey);
}
prefetchSingleMode(currentPageNum);
}
// =========================================================================
// 4. BIDIRECTIONAL STRIP MODE ENGINE (RTL & VERTICAL WITH PLACEHOLDERS)
// =========================================================================
function getOrCreateStripContainer() {
if (!isReaderPage) return null;
let container = document.getElementById('eh-strip-container');
if (!container) {
container = document.createElement('div');
container.id = 'eh-strip-container';
container.className = `eh-strip-container layout-${currentLayout}`;
const i3 = document.getElementById('i3');
if (i3 && i3.parentNode) {
i3.parentNode.insertBefore(container, i3.nextSibling);
} else {
const i1 = document.getElementById('i1') || document.body;
i1.appendChild(container);
}
container.addEventListener('scroll', scheduleScrollUpdate, { passive: true });
container.addEventListener('pointerover', (e) => {
const item = e.target.closest('.eh-strip-item');
if (item && item.dataset.page) {
const page = parseInt(item.dataset.page, 10);
if (page && isPageEligibleToLoad(page)) {
checkAndLoadNearbyPages(page);
}
}
}, { passive: true });
}
return container;
}
function ensureStartSpacer() {
const container = getOrCreateStripContainer();
let spacer = document.getElementById('eh-strip-start-spacer');
if (!spacer) {
spacer = document.createElement('div');
spacer.id = 'eh-strip-start-spacer';
spacer.className = 'eh-strip-start-spacer';
if (container.firstChild) {
container.insertBefore(spacer, container.firstChild);
} else {
container.appendChild(spacer);
}
} else if (spacer !== container.firstChild) {
container.insertBefore(spacer, container.firstChild);
}
return spacer;
}
function ensureBottomGalleryButton() {
if (!stripActive) return;
const url = getGalleryUrl();
if (currentLayout === 'vertical') {
const existingEndCard = document.getElementById('eh-strip-end-card');
if (existingEndCard) existingEndCard.remove();
let bar = document.getElementById('eh-bottom-gallery-bar');
if (!bar) {
bar = document.createElement('div');
bar.id = 'eh-bottom-gallery-bar';
bar.className = 'eh-bottom-gallery-bar';
bar.innerHTML = `<a href="${url}">← Return to Gallery</a>`;
const container = getOrCreateStripContainer();
if (container.parentNode) {
container.parentNode.insertBefore(bar, container.nextSibling);
}
} else {
const link = bar.querySelector('a');
if (link) link.href = url;
}
} else {
const existingBar = document.getElementById('eh-bottom-gallery-bar');
if (existingBar) existingBar.remove();
const container = getOrCreateStripContainer();
let endCard = document.getElementById('eh-strip-end-card');
if (!endCard) {
endCard = document.createElement('div');
endCard.id = 'eh-strip-end-card';
endCard.className = 'eh-strip-item eh-strip-end-card';
endCard.innerHTML = `<div class="eh-bottom-gallery-bar"><a href="${url}">← Return to Gallery</a></div>`;
container.appendChild(endCard);
} else {
const link = endCard.querySelector('a');
if (link) link.href = url;
if (endCard.nextSibling) {
container.appendChild(endCard);
}
}
}
}
function applyItemDimensions(item, pageNo) {
const dim = pageDimensions.get(pageNo);
const ar = dim ? dim.aspectRatio : defaultAspectRatio;
item.style.setProperty('--eh-ar', String(ar));
item.style.aspectRatio = String(ar);
if (currentLayout === 'horizontal') {
item.style.width = `calc(100vh * ${ar})`;
item.style.maxWidth = '';
} else {
item.style.width = '100%';
if (dim && dim.width && dim.width > 500) {
item.style.setProperty('--eh-max-w', dim.width + 'px');
} else {
item.style.setProperty('--eh-max-w', '100%');
}
}
}
function renderPlaceholderContent(item, pageNo, retryCount = 0) {
item.classList.add('eh-placeholder');
if (loadingPages.has(pageNo)) {
item.classList.add('eh-loading');
} else {
item.classList.remove('eh-loading');
}
const oldContent = item.querySelector('.eh-placeholder-content');
if (oldContent) oldContent.remove();
applyThumbnailToPlaceholder(item, pageNo);
// Only show the spinner & status text if the image is having trouble loading
const failInfo = failedPages.get(pageNo);
const isTroubled = (retryCount > 0) || (failInfo && failInfo.count > 0);
if (isTroubled) {
if (failInfo && failInfo.count >= MAX_RETRIES) {
showRetryUI(item, pageNo);
} else {
const content = document.createElement('div');
content.className = 'eh-placeholder-content';
const spinner = document.createElement('div');
spinner.className = 'eh-spinner';
const text = document.createElement('div');
text.className = 'eh-placeholder-text';
text.textContent = `Retrying Page ${pageNo}...`;
content.appendChild(spinner);
content.appendChild(text);
item.appendChild(content);
}
}
}
function ensureAllPlaceholders(tot) {
if (!isReaderPage || !stripActive || !tot || tot <= 0) return;
const container = getOrCreateStripContainer();
let added = false;
withScrollAnchor(() => {
ensureStartSpacer();
const obs = getStripItemObserver();
const endCard = document.getElementById('eh-strip-end-card');
let batchFrag = null;
let batchNextEl = null;
const flushBatch = () => {
if (batchFrag && batchFrag.childNodes.length > 0) {
if (batchNextEl && batchNextEl.isConnected) {
container.insertBefore(batchFrag, batchNextEl);
} else if (endCard && endCard.isConnected) {
container.insertBefore(batchFrag, endCard);
} else {
container.appendChild(batchFrag);
}
batchFrag = null;
batchNextEl = null;
}
};
for (let p = 1; p <= tot; p++) {
if (pageItemMap.has(p)) {
flushBatch();
continue;
}
const item = document.createElement('div');
item.className = 'eh-strip-item eh-placeholder';
item.dataset.page = String(p);
applyItemDimensions(item, p);
renderPlaceholderContent(item, p);
pageItemMap.set(p, item);
if (obs) obs.observe(item);
if (!batchFrag) {
batchFrag = document.createDocumentFragment();
let nextEl = null;
for (let nextP = p + 1; nextP <= tot; nextP++) {
if (pageItemMap.has(nextP)) {
const el = pageItemMap.get(nextP);
if (el && el.isConnected) {
nextEl = el;
break;
}
}
}
batchNextEl = nextEl;
}
batchFrag.appendChild(item);
added = true;
}
flushBatch();
});
if (added) {
syncPageIndicator(currentPageNum);
}
}
function createOrGetPlaceholder(pageNo) {
if (!isReaderPage) return null;
const container = getOrCreateStripContainer();
if (!container) return null;
let item = pageItemMap.get(pageNo);
if (item) return item;
item = document.createElement('div');
item.className = 'eh-strip-item eh-placeholder';
item.dataset.page = String(pageNo);
applyItemDimensions(item, pageNo);
renderPlaceholderContent(item, pageNo);
pageItemMap.set(pageNo, item);
const obs = getStripItemObserver();
if (obs) obs.observe(item);
withScrollAnchor(() => {
ensureStartSpacer();
const existing = container.querySelectorAll('.eh-strip-item:not(.eh-strip-end-card)');
let inserted = false;
for (const el of existing) {
const p = parseInt(el.dataset.page, 10);
if (p > pageNo) {
container.insertBefore(item, el);
inserted = true;
break;
}
}
if (!inserted) {
const endCard = document.getElementById('eh-strip-end-card');
if (endCard) {
container.insertBefore(item, endCard);
} else {
container.appendChild(item);
}
}
});
return item;
}
function renderImageInItem(pageNo, imgSrc, retryCount = 0) {
let item = pageItemMap.get(pageNo);
if (!item) {
item = createOrGetPlaceholder(pageNo);
}
const existingImg = item.querySelector('img.eh-main-img');
if (existingImg && existingImg.src === imgSrc && loadedImages.has(pageNo)) {
loadingPages.delete(pageNo);
setTimeout(() => { if (stripActive) checkAndLoadNearbyPages(); }, 10);
return;
}
const img = document.createElement('img');
img.alt = `Page ${pageNo}`;
img.className = 'eh-main-img';
img.decoding = 'async';
const startTime = Date.now();
const dim = pageDimensions.get(pageNo);
if (dim && dim.width && dim.height && dim.width > 500) {
img.width = dim.width;
img.height = dim.height;
}
item._currentImgToken = img;
let slowTimerId = null;
let hardTimerId = null;
let isSettled = false;
const cleanupTimers = () => {
if (slowTimerId) { clearTimeout(slowTimerId); slowTimerId = null; }
if (hardTimerId) { clearTimeout(hardTimerId); hardTimerId = null; }
};
const cleanupImg = () => {
cleanupTimers();
img.onload = null;
img.onerror = null;
};
const abortImg = () => {
isSettled = true;
cleanupImg();
img.removeAttribute('src');
img.src = '';
};
item._abortImg = abortImg;
const handleFailure = (reason, isSlowStall = false) => {
if (isSettled) return;
isSettled = true;
item._abortImg = null;
cleanupImg();
// Release socket connection immediately
img.removeAttribute('src');
img.src = '';
loadingPages.delete(pageNo);
if (item) item.classList.remove('eh-loading');
setTimeout(() => { if (stripActive) checkAndLoadNearbyPages(); }, 10);
if (item._currentImgToken !== img) return;
console.warn(`ExFastReader: Image for page ${pageNo} ${reason}, attempt ${retryCount + 1}/${MAX_RETRIES}`);
if (retryCount < MAX_RETRIES) {
loadingPages.set(pageNo, Date.now());
const delay = isSlowStall ? 100 : Math.min(2000, 400 * Math.pow(1.5, retryCount));
setTimeout(() => {
if (!stripActive) {
loadingPages.delete(pageNo);
return;
}
reloadPageImage(pageNo, retryCount + 1);
}, delay);
} else {
loadingPages.delete(pageNo);
loadedImages.delete(pageNo);
recordPageFailure(pageNo, reason);
showRetryUI(item, pageNo);
setTimeout(() => { if (stripActive) checkAndLoadNearbyPages(); }, 10);
}
};
img.onload = async () => {
if (!stripActive || isSettled) return;
isSettled = true;
item._abortImg = null;
cleanupImg();
loadingPages.delete(pageNo);
if (item) item.classList.remove('eh-loading');
setTimeout(() => { if (stripActive) checkAndLoadNearbyPages(); }, 10);
if (item._currentImgToken !== img) return;
try {
if ('decode' in img) await img.decode();
} catch {}
if (item._currentImgToken !== img) return;
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
updatePageDimensions(pageNo, img.naturalWidth, img.naturalHeight);
}
withScrollAnchor(() => {
item.classList.remove('eh-placeholder');
const content = item.querySelector('.eh-placeholder-content');
if (content) content.remove();
const thumbs = item.querySelectorAll('.eh-placeholder-thumb-img, .eh-placeholder-thumb-wrap');
thumbs.forEach(t => {
if (t.tagName === 'IMG') {
t.onload = null;
t.onerror = null;
t.removeAttribute('src');
t.src = '';
} else {
t.querySelectorAll('img').forEach(i => { i.onload = null; i.onerror = null; i.removeAttribute('src'); i.src = ''; });
}
t.remove();
});
const oldImg = item.querySelector('img.eh-main-img');
if (oldImg && oldImg !== img) {
oldImg.onload = null;
oldImg.onerror = null;
oldImg.removeAttribute('src');
oldImg.src = '';
oldImg.remove();
}
if (!item.contains(img)) {
item.appendChild(img);
}
}, pageNo);
loadedImages.add(pageNo);
failedPages.delete(pageNo);
const durationSec = Math.max(0.1, (Date.now() - startTime) / 1000);
recordDownloadSpeedSample(pageNo, durationSec);
if (currentPageNum === pageNo) {
updateTopBarInfo(pageNo);
}
};
img.onerror = () => {
handleFailure('network load error', false);
};
// Dynamic stall timeouts based on image file size and measured download speed
const { fileSize, speed, slowTimeoutMs, hardTimeoutMs } = calculateImageTimeout(pageNo);
slowTimerId = setTimeout(() => {
if (!img.complete || img.naturalWidth === 0) {
console.warn(`ExFastReader: Page ${pageNo} (${(fileSize / 1024).toFixed(0)} KB) is exceptionally slow (> ${(slowTimeoutMs / 1000).toFixed(1)}s at ${(speed / (1024 * 1024)).toFixed(2)} MB/s). Triggering failover bounce.`);
if (retryCount < MAX_RETRIES) {
handleFailure(`exceptionally slow (> ${(slowTimeoutMs / 1000).toFixed(1)}s)`, true);
}
}
}, slowTimeoutMs);
// Hard stall timeout safeguard based on file size and download speed
hardTimerId = setTimeout(() => {
if (!img.complete || img.naturalWidth === 0) {
handleFailure(`hard stalled (> ${(hardTimeoutMs / 1000).toFixed(1)}s)`, false);
}
}, hardTimeoutMs);
img.src = imgSrc;
}
function showRetryUI(item, pageNo) {
item.classList.add('eh-placeholder');
applyThumbnailToPlaceholder(item, pageNo);
const oldContent = item.querySelector('.eh-placeholder-content');
if (oldContent) oldContent.remove();
const content = document.createElement('div');
content.className = 'eh-placeholder-content eh-placeholder-retry';
content.style.cursor = 'pointer';
content.innerHTML = `
<div style="font-size: 20px;">⚠️</div>
<div class="eh-placeholder-text">Page ${pageNo} failed to load.</div>
<button type="button" class="eh-strip-btn active" style="margin-top: 4px;">Click to switch server & retry</button>
`;
content.onclick = (e) => {
e.stopPropagation();
content.onclick = null;
failedPages.delete(pageNo);
reloadPageImage(pageNo, 0);
};
item.appendChild(content);
}
async function reloadPageImage(pageNo, retryCount = 0) {
const item = pageItemMap.get(pageNo);
if (item && !item.querySelector('img.eh-main-img')) {
renderPlaceholderContent(item, pageNo);
}
loadedImages.delete(pageNo);
loadingPages.set(pageNo, Date.now()); // Mark loading to prevent duplicate concurrent load
failedPages.delete(pageNo);
pageImgSrcCache.delete(pageNo);
pageRespCache.delete(pageNo);
const nl = pageNlCodes.get(pageNo);
const imgKey = pageImgKeyCache.get(pageNo);
if (imgKey) {
try {
const freshResp = await fetchApi(pageNo, imgKey, nl || null, true);
if (freshResp) {
pageRespCache.set(pageNo, freshResp);
extractKeysFromResp(freshResp, pageNo);
const extracted = extractInfoFromResp(freshResp, pageNo);
if (extracted) pageTopBarInfoCache.set(pageNo, extracted);
if (currentPageNum === pageNo) updateTopBarInfo(pageNo);
const newNl = extractNlCode(freshResp);
if (newNl) {
pageNlCodes.set(pageNo, newNl);
}
if (freshResp.i3) {
const i3Doc = parseTemplate(freshResp.i3);
const imgEl = i3Doc.querySelector('img#img') || i3Doc.querySelector('img');
const freshSrc = imgEl ? (imgEl.getAttribute('src') || imgEl.src) : null;
if (freshSrc) {
pageImgSrcCache.set(pageNo, freshSrc);
renderImageInItem(pageNo, freshSrc, retryCount);
return;
}
}
}
} catch (err) {
console.warn(`ExFastReader: Failed to reload page ${pageNo}:`, err);
}
}
loadPage(pageNo, retryCount);
}
function isPageEligibleToLoad(pageNo) {
if (!pageNo || isNaN(pageNo) || pageNo < 1) return false;
const tot = getTotalPages();
if (tot > 0 && pageNo > tot) return false;
if (loadedImages.has(pageNo) || loadingPages.has(pageNo)) return false;
const failInfo = failedPages.get(pageNo);
if (failInfo) {
if (failInfo.count >= 3) return false;
const cooldown = Math.min(10000, 1000 * Math.pow(2, failInfo.count - 1));
if (Date.now() - failInfo.lastAttempt < cooldown) {
return false;
}
}
return true;
}
function recordPageFailure(pageNo, reason) {
const current = failedPages.get(pageNo) || { count: 0, lastAttempt: 0 };
current.count += 1;
current.lastAttempt = Date.now();
failedPages.set(pageNo, current);
console.warn(`ExFastReader: Page ${pageNo} failed (attempt ${current.count}): ${reason}`);
if (current.count >= 3) {
const item = pageItemMap.get(pageNo);
if (item && !item.querySelector('img.eh-main-img')) {
showRetryUI(item, pageNo);
}
}
}
function getVisiblePagesAndCurrent() {
const vpW = window.innerWidth;
const vpH = window.innerHeight;
const vpCenterX = vpW / 2;
const vpCenterY = vpH / 2;
const isHoriz = currentLayout === 'horizontal';
if (pageItemMap.size === 0) {
return { visible: [currentPageNum || 1], current: currentPageNum || 1 };
}
// 1. Instant O(1) probe at viewport center
let seedPage = null;
const centerEl = document.elementFromPoint(vpCenterX, vpCenterY)?.closest('.eh-strip-item');
if (centerEl && centerEl.dataset.page) {
const p = parseInt(centerEl.dataset.page, 10);
if (p && pageItemMap.has(p)) seedPage = p;
}
// 2. Fast check around currentPageNum or immediate neighbors
if (!seedPage && currentPageNum && pageItemMap.has(currentPageNum)) {
for (const testP of [currentPageNum, currentPageNum - 1, currentPageNum + 1]) {
const item = pageItemMap.get(testP);
if (item && item.isConnected) {
const r = item.getBoundingClientRect();
const inView = isHoriz ? (r.right > 0 && r.left < vpW) : (r.bottom > 0 && r.top < vpH);
if (inView) {
seedPage = testP;
break;
}
}
}
}
// 3. Fallback: monotonic binary search over pageItemMap keys (log2(N) <= 9 probes for 500 items)
if (!seedPage) {
const pages = Array.from(pageItemMap.keys()).sort((a, b) => a - b);
let low = 0, high = pages.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const p = pages[mid];
const item = pageItemMap.get(p);
if (!item || !item.isConnected) {
low = mid + 1;
continue;
}
const r = item.getBoundingClientRect();
if (isHoriz) {
// In RTL flex-row: earlier pages (page 1) are on the right (higher X).
if (r.left >= vpW) {
low = mid + 1;
} else if (r.right <= 0) {
high = mid - 1;
} else {
seedPage = p;
break;
}
} else {
if (r.bottom <= 0) {
low = mid + 1;
} else if (r.top >= vpH) {
high = mid - 1;
} else {
seedPage = p;
break;
}
}
}
}
if (!seedPage) {
seedPage = currentPageNum || 1;
}
const visible = [];
let bestPage = seedPage;
let minCenterDist = Infinity;
let centerCoveredPage = null;
function testPage(p) {
const item = pageItemMap.get(p);
if (!item || !item.isConnected) return false;
const r = item.getBoundingClientRect();
if (isHoriz) {
if (r.right > 0 && r.left < vpW) {
visible.push(p);
if (r.left <= vpCenterX && r.right >= vpCenterX) centerCoveredPage = p;
const dist = Math.abs((r.left + r.right) / 2 - vpCenterX);
if (dist < minCenterDist) {
minCenterDist = dist;
bestPage = p;
}
return true;
}
} else {
if (r.bottom > 0 && r.top < vpH) {
visible.push(p);
if (r.top <= vpCenterY && r.bottom >= vpCenterY) centerCoveredPage = p;
const dist = Math.abs((r.top + r.bottom) / 2 - vpCenterY);
if (dist < minCenterDist) {
minCenterDist = dist;
bestPage = p;
}
return true;
}
}
return false;
}
testPage(seedPage);
// Expand outward backwards
for (let p = seedPage - 1; p >= 1; p--) {
if (!testPage(p)) break;
}
// Expand outward forwards
const total = getTotalPages() || (seedPage + 100);
for (let p = seedPage + 1; p <= total; p++) {
if (!testPage(p)) break;
}
if (visible.length === 0) {
visible.push(bestPage);
}
visible.sort((a, b) => a - b);
const current = centerCoveredPage !== null ? centerCoveredPage : bestPage;
return { visible, current };
}
function getVisiblePageNums() {
return getVisiblePagesAndCurrent().visible;
}
function getCurrentlyVisiblePageNum() {
return getVisiblePagesAndCurrent().current;
}
function checkAndLoadNearbyPages(targetPage, precomputedVisible = null) {
if (!isReaderPage || !stripActive || isPumpingQueue) return;
isPumpingQueue = true;
try {
const cur = targetPage || currentPageNum;
const total = getTotalPages();
prefetchNearbyThumbnails(cur);
const candidates = [];
const visiblePages = precomputedVisible || getVisiblePageNums();
const sortedVisible = scrollDirection === 'backward' ? [...visiblePages].reverse() : visiblePages;
for (const vp of sortedVisible) {
if (!candidates.includes(vp)) candidates.push(vp);
}
if (!candidates.includes(cur)) candidates.push(cur);
if (scrollDirection === 'backward') {
const backDepth = PREFETCH_DEPTH + 1;
for (let b = 1; b <= backDepth; b++) {
const p = cur - b;
if (p >= 1 && (!total || p <= total) && !candidates.includes(p)) {
candidates.push(p);
}
}
for (let f = 1; f <= 2; f++) {
const p = cur + f;
if (p >= 1 && (!total || p <= total) && !candidates.includes(p)) {
candidates.push(p);
}
}
} else {
const forwardDepth = currentLayout === 'horizontal' ? (PREFETCH_DEPTH + 1) : PREFETCH_DEPTH;
for (let f = 1; f <= forwardDepth; f++) {
const p = cur + f;
if (p >= 1 && (!total || p <= total) && !candidates.includes(p)) {
candidates.push(p);
}
}
for (let b = 1; b <= 2; b++) {
const p = cur - b;
if (p >= 1 && (!total || p <= total) && !candidates.includes(p)) {
candidates.push(p);
}
}
}
for (const pageNo of candidates) {
const isVisible = visiblePages.includes(pageNo) || pageNo === cur;
const limit = isVisible ? (MAX_CONCURRENT_LOADS + 2) : MAX_CONCURRENT_LOADS;
if (loadingPages.size >= limit) {
if (!isVisible) break;
}
if (isPageEligibleToLoad(pageNo)) {
loadPage(pageNo, 0);
}
}
} finally {
isPumpingQueue = false;
}
}
async function loadPage(pageNo, retryCount = 0) {
if (!isReaderPage || !isPageEligibleToLoad(pageNo)) return;
loadingPages.set(pageNo, Date.now());
let renderStarted = false;
const item = pageItemMap.get(pageNo);
if (item) item.classList.add('eh-loading');
try {
let imgKey = pageImgKeyCache.get(pageNo);
if (!imgKey) {
imgKey = await ensureKeysForPage(pageNo);
}
if (!imgKey) {
recordPageFailure(pageNo, 'Missing image key');
return;
}
if (!stripActive) {
return;
}
let imgSrc = pageImgSrcCache.get(pageNo);
if (!imgSrc) {
let resp = pageRespCache.get(pageNo);
if (!resp) {
resp = await fetchApi(pageNo, imgKey);
if (resp && !resp.error) {
pageRespCache.set(pageNo, resp);
}
}
if (!resp) {
recordPageFailure(pageNo, 'API request timed out or network error');
return;
}
if (resp.error) {
recordPageFailure(pageNo, `API error: ${resp.error}`);
return;
}
if (!resp.i3) {
recordPageFailure(pageNo, 'API response missing i3 image container');
return;
}
if (!resp || resp.error) {
// If throttled or errored, retry after a short delay
if (retryCount < MAX_RETRIES) {
const backoff = 1000 * (retryCount + 1);
setTimeout(() => {
if (stripActive) reloadPageImage(pageNo, retryCount + 1);
}, backoff);
return;
}
}
if (!stripActive) {
return;
}
const newNl = extractNlCode(resp);
if (newNl) {
pageNlCodes.set(pageNo, newNl);
}
const i3Doc = parseTemplate(resp.i3);
const imgEl = i3Doc.querySelector('img#img');
imgSrc = imgEl ? (imgEl.getAttribute('src') || imgEl.src) : null;
if (imgSrc) {
pageImgSrcCache.set(pageNo, imgSrc);
}
}
if (imgSrc) {
renderStarted = true;
renderImageInItem(pageNo, imgSrc, retryCount);
} else {
recordPageFailure(pageNo, 'Image element missing src');
}
} catch (err) {
recordPageFailure(pageNo, err.message || 'Exception during load');
} finally {
if (!renderStarted) {
loadingPages.delete(pageNo);
const it = pageItemMap.get(pageNo);
if (it) it.classList.remove('eh-loading');
}
setTimeout(() => {
if (stripActive) checkAndLoadNearbyPages();
}, 50);
}
}
function scheduleScrollUpdate() {
if (!isReaderPage || !stripActive || scrollRafId || isTransitioningLayout) return;
scrollRafId = requestAnimationFrame(() => {
scrollRafId = null;
onScrollThrottled();
});
}
function onScrollThrottled() {
if (!isReaderPage || !stripActive || isTransitioningLayout) return;
if (!animScrollRaf) targetScrollLeft = null;
if (!animScrollTopRaf) targetScrollTop = null;
const container = document.getElementById('eh-strip-container');
if (currentLayout === 'horizontal' && container) {
const curLeft = container.scrollLeft;
if (curLeft > lastScrollLeft + 2) {
scrollDirection = 'backward';
} else if (curLeft < lastScrollLeft - 2) {
scrollDirection = 'forward';
}
lastScrollLeft = curLeft;
} else {
const curY = window.scrollY;
if (curY < lastScrollY - 2) {
scrollDirection = 'backward';
} else if (curY > lastScrollY + 2) {
scrollDirection = 'forward';
}
lastScrollY = curY;
}
const { visible: visiblePages, current: activePage } = getVisiblePagesAndCurrent();
if (activePage < currentPageNum) {
scrollDirection = 'backward';
} else if (activePage > currentPageNum) {
scrollDirection = 'forward';
}
currentPageNum = activePage;
syncPageIndicator(activePage);
updateTopBarInfo(activePage);
if (activePage !== lastUpdatePage) {
lastUpdatePage = activePage;
pruneCaches(activePage);
}
checkAndLoadNearbyPages(activePage, visiblePages);
prefetchNearbyThumbnails(activePage);
if (activePage !== lastHistoryPage) {
lastHistoryPage = activePage;
if (historyDebounceTimer) clearTimeout(historyDebounceTimer);
historyDebounceTimer = setTimeout(() => {
if (pageRespCache.has(activePage)) {
const resp = pageRespCache.get(activePage);
const targetUrl = (window.base_url || '/') + resp.s;
if (location.pathname !== '/' + resp.s) {
window.history.replaceState({ page: activePage }, document.title, targetUrl);
}
}
}, 300);
}
}
function syncPageIndicator(activePage, force = false) {
if (!force && activePage === lastSyncedIndicatorPage) return;
lastSyncedIndicatorPage = activePage;
const total = getTotalPages();
const pageDisplay = document.getElementById('eh-strip-page-display');
if (pageDisplay) {
pageDisplay.textContent = total ? `Page ${activePage} / ${total}` : `Page ${activePage}`;
}
const curSpan = document.querySelector('#i2 .sn span:first-child');
if (curSpan && curSpan.textContent !== String(activePage)) {
curSpan.textContent = activePage;
}
const galUrl = getGalleryUrl(activePage);
const galleryBtn = document.getElementById('eh-strip-gallery-btn');
if (galleryBtn) {
galleryBtn.href = galUrl;
}
const bottomLinks = document.querySelectorAll('#eh-bottom-gallery-bar a, #eh-strip-end-card a');
for (const a of bottomLinks) {
a.href = galUrl;
}
}
function enableStripMode() {
stripActive = true;
safeStorageSet(localStorage, 'eh_strip_mode_enabled', '1');
setStripClasses(true, currentLayout);
updateToggleButtonUI();
const container = getOrCreateStripContainer();
container.className = `eh-strip-container layout-${currentLayout}`;
const initialMeta = getCurrentPageMeta();
currentPageNum = initialMeta.page;
if (initialMeta.imgKey) {
pageImgKeyCache.set(currentPageNum, initialMeta.imgKey);
}
detectInitialPageDimensions();
applyGlobalAspectRatio(defaultAspectRatio);
extractKeysFromDoc(document);
const initNl = extractInitialNlCode(document);
if (initNl) {
pageNlCodes.set(currentPageNum, initNl);
}
extractAndHideBottomLinks();
ensureStartSpacer();
const totalPages = getTotalPages();
if (totalPages > 0) {
const frag = document.createDocumentFragment();
for (let p = 1; p <= totalPages; p++) {
if (!pageItemMap.has(p)) {
const item = document.createElement('div');
item.className = 'eh-strip-item eh-placeholder';
item.dataset.page = String(p);
applyItemDimensions(item, p);
renderPlaceholderContent(item, p);
pageItemMap.set(p, item);
frag.appendChild(item);
}
}
const endCard = document.getElementById('eh-strip-end-card');
if (endCard) {
container.insertBefore(frag, endCard);
} else {
container.appendChild(frag);
}
} else {
createOrGetPlaceholder(currentPageNum);
}
const currentImg = document.getElementById('img');
if (currentImg && currentImg.src) {
pageImgSrcCache.set(currentPageNum, currentImg.src);
if (!loadedImages.has(currentPageNum)) {
renderImageInItem(currentPageNum, currentImg.src, 0);
}
}
ensureBottomGalleryButton();
discoverAndFetchGallery(currentPageNum);
isTransitioningLayout = true;
instantScrollToPage(currentPageNum);
const obs = getStripItemObserver();
if (obs) {
for (const it of pageItemMap.values()) {
obs.observe(it);
}
}
isTransitioningLayout = false;
scheduleScrollUpdate();
checkAndLoadNearbyPages(currentPageNum);
lastUpdatePage = currentPageNum;
syncPageIndicator(currentPageNum);
updateTopBarInfo(currentPageNum);
}
function disableStripMode() {
stripActive = false;
cancelKeyboardAnimation();
safeStorageSet(localStorage, 'eh_strip_mode_enabled', '0');
if (historyDebounceTimer) {
clearTimeout(historyDebounceTimer);
historyDebounceTimer = null;
}
if (scrollRafId) {
cancelAnimationFrame(scrollRafId);
scrollRafId = null;
}
if (stripItemObserver) {
stripItemObserver.disconnect();
stripItemObserver = null;
}
setStripClasses(false, currentLayout);
updateToggleButtonUI();
for (const item of pageItemMap.values()) {
if (typeof item._abortImg === 'function') {
item._abortImg();
item._abortImg = null;
}
item._currentImgToken = null;
const allImgs = item.querySelectorAll('img');
allImgs.forEach(img => {
img.onload = null;
img.onerror = null;
img.removeAttribute('src');
img.src = '';
img.remove();
});
const retryContents = item.querySelectorAll('.eh-placeholder-retry, .eh-strip-btn');
retryContents.forEach(el => { el.onclick = null; });
}
for (const [page, record] of preloadedImages.entries()) {
if (record && record.img) {
record.img.onload = null;
record.img.onerror = null;
record.img.removeAttribute('src');
record.img.src = '';
}
}
preloadedImages.clear();
const container = document.getElementById('eh-strip-container');
if (container) {
container.removeEventListener('scroll', scheduleScrollUpdate);
container.remove();
}
const bottomBar = document.getElementById('eh-bottom-gallery-bar');
if (bottomBar) bottomBar.remove();
const startSpacer = document.getElementById('eh-strip-start-spacer');
if (startSpacer) startSpacer.remove();
pageItemMap.clear();
loadedImages.clear();
loadingPages.clear();
failedPages.clear();
inFlightFetches.clear();
inFlightGalleryFetches.clear();
if (pageRespCache.has(currentPageNum)) {
const resp = pageRespCache.get(currentPageNum);
if (typeof window.apply_json_state === 'function') {
window.apply_json_state(resp);
}
} else if (pageImgKeyCache.has(currentPageNum) && typeof window.load_image === 'function') {
window.load_image(currentPageNum, pageImgKeyCache.get(currentPageNum));
}
initImageMonitor();
startPrefetch();
}
function toggleStripMode() {
if (stripActive) {
disableStripMode();
} else {
enableStripMode();
}
}
function toggleStripLayout() {
const targetPage = currentPageNum || getCurrentlyVisiblePageNum() || 1;
cancelKeyboardAnimation();
isTransitioningLayout = true;
currentLayout = currentLayout === 'horizontal' ? 'vertical' : 'horizontal';
safeStorageSet(localStorage, 'eh_strip_layout', currentLayout);
setStripClasses(true, currentLayout);
const container = document.getElementById('eh-strip-container');
if (container) {
container.className = `eh-strip-container layout-${currentLayout}`;
if (currentLayout === 'vertical') {
container.scrollLeft = 0;
}
for (const [p, item] of pageItemMap.entries()) {
applyItemDimensions(item, p);
}
}
if (stripItemObserver) {
stripItemObserver.disconnect();
stripItemObserver = null;
}
const obs = getStripItemObserver();
if (obs) {
for (const item of pageItemMap.values()) {
obs.observe(item);
}
}
ensureBottomGalleryButton();
updateToggleButtonUI();
currentPageNum = targetPage;
lastUpdatePage = targetPage;
syncPageIndicator(targetPage);
updateTopBarInfo(targetPage);
if (container) void container.offsetHeight;
instantScrollToPage(targetPage);
requestAnimationFrame(() => {
instantScrollToPage(targetPage);
setTimeout(() => {
instantScrollToPage(targetPage);
currentPageNum = targetPage;
lastUpdatePage = targetPage;
syncPageIndicator(targetPage);
isTransitioningLayout = false;
scheduleScrollUpdate();
checkAndLoadNearbyPages(targetPage);
}, 60);
});
}
function getSingleModeLowerArea() {
const i5 = document.getElementById('i5');
const i6 = document.getElementById('i6');
const i7 = document.getElementById('i7');
const loadfail = document.getElementById('loadfail');
const loadfailParent = loadfail ? loadfail.closest('.idi, div, p') : null;
let textMatch = null;
const candidates = document.querySelectorAll('#i1 > div, #i1 > p, div.idi');
for (const el of candidates) {
const txt = (el.textContent || '').toLowerCase();
if (txt.includes('download original') || txt.includes('reload broken image') || txt.includes('show galleries')) {
textMatch = el;
break;
}
}
return i5 || i6 || i7 || loadfailParent || textMatch;
}
function placeStripControls() {
let controls = document.getElementById('eh-strip-controls');
if (!controls) return;
if (stripActive) {
controls.classList.remove('single-mode-bottom');
const i2 = document.getElementById('i2');
if (i2 && controls.parentNode !== i2) {
const topbarInfo = document.getElementById('eh-topbar-info');
if (topbarInfo) {
i2.insertBefore(controls, topbarInfo.nextSibling);
} else {
i2.appendChild(controls);
}
}
} else {
controls.classList.add('single-mode-bottom');
// Locate the 30x30px icon container that links back to the gallery (#i5 / div.sb)
const galleryIcon = document.getElementById('i5') ||
document.querySelector('div.sb') ||
document.querySelector('#i1 a[href*="/g/"] img')?.closest('div, p');
if (galleryIcon && galleryIcon.parentNode) {
// Insert directly underneath (after) the 30x30px gallery icon
if (galleryIcon.nextElementSibling !== controls || controls.parentNode !== galleryIcon.parentNode) {
galleryIcon.insertAdjacentElement('afterend', controls);
}
} else {
// Fallback placement if the gallery icon container isn't found
const lowerArea = getSingleModeLowerArea();
if (lowerArea && lowerArea.parentNode) {
if (controls.nextSibling !== lowerArea || controls.parentNode !== lowerArea.parentNode) {
lowerArea.parentNode.insertBefore(controls, lowerArea);
}
} else {
const i4 = document.getElementById('i4');
if (i4 && i4.parentNode) {
if (i4.nextElementSibling !== controls || controls.parentNode !== i4.parentNode) {
i4.insertAdjacentElement('afterend', controls);
}
} else {
const i1 = document.getElementById('i1') || document.body;
if (controls.parentNode !== i1) {
i1.appendChild(controls);
}
}
}
}
}
}
function updateToggleButtonUI() {
const modeBtn = document.getElementById('eh-strip-mode-toggle');
const statusText = document.getElementById('eh-strip-status');
const dirBtn = document.getElementById('eh-strip-dir-toggle');
const dirText = document.getElementById('eh-strip-dir-text');
const pageDisplay = document.getElementById('eh-strip-page-display');
const galleryBtn = document.getElementById('eh-strip-gallery-btn');
placeStripControls();
if (modeBtn && statusText) {
if (stripActive) {
modeBtn.classList.add('active');
statusText.textContent = 'ON';
} else {
modeBtn.classList.remove('active');
statusText.textContent = 'OFF';
}
}
if (dirBtn && dirText) {
dirBtn.style.display = stripActive ? 'inline-flex' : 'none';
dirText.textContent = currentLayout === 'horizontal' ? 'Horizontal (RTL)' : 'Vertical';
}
if (pageDisplay) {
pageDisplay.style.display = stripActive ? 'inline-block' : 'none';
}
if (galleryBtn) {
galleryBtn.style.display = stripActive ? 'inline-flex' : 'none';
galleryBtn.href = getGalleryUrl();
}
const frontBtn = document.getElementById('eh-strip-front-btn');
const onionBtn = document.getElementById('eh-strip-onion-btn');
if (frontBtn) {
frontBtn.style.display = stripActive ? 'inline-flex' : 'none';
frontBtn.href = cachedFrontUrl || (location.origin + '/');
}
if (onionBtn) {
onionBtn.style.display = stripActive && cachedOnionUrl ? 'inline-flex' : 'none';
if (cachedOnionUrl) onionBtn.href = cachedOnionUrl;
}
}
function injectReaderToggle() {
let controls = document.getElementById('eh-strip-controls');
if (!controls) {
controls = document.createElement('div');
controls.id = 'eh-strip-controls';
controls.className = 'eh-strip-controls';
const modeBtn = document.createElement('button');
modeBtn.type = 'button';
modeBtn.id = 'eh-strip-mode-toggle';
modeBtn.className = 'eh-strip-btn' + (stripActive ? ' active' : '');
modeBtn.title = 'Toggle Seamless Strip Reading Mode';
modeBtn.innerHTML = `Strip Mode: <span id="eh-strip-status">${stripActive ? 'ON' : 'OFF'}</span>`;
modeBtn.addEventListener('click', toggleStripMode);
const dirBtn = document.createElement('button');
dirBtn.type = 'button';
dirBtn.id = 'eh-strip-dir-toggle';
dirBtn.className = 'eh-strip-btn';
dirBtn.title = 'Switch Orientation (Horizontal RTL / Vertical)';
dirBtn.style.display = stripActive ? 'inline-flex' : 'none';
dirBtn.innerHTML = `<span id="eh-strip-dir-text">${currentLayout === 'horizontal' ? 'Horizontal (RTL)' : 'Vertical'}</span>`;
dirBtn.addEventListener('click', toggleStripLayout);
const galleryBtn = document.createElement('a');
galleryBtn.id = 'eh-strip-gallery-btn';
galleryBtn.className = 'eh-strip-btn';
galleryBtn.title = 'Return to Gallery';
galleryBtn.href = getGalleryUrl();
galleryBtn.textContent = 'Gallery';
galleryBtn.style.display = stripActive ? 'inline-flex' : 'none';
const frontBtn = document.createElement('a');
frontBtn.id = 'eh-strip-front-btn';
frontBtn.className = 'eh-strip-btn';
frontBtn.title = 'Front Page';
frontBtn.href = cachedFrontUrl || (location.origin + '/');
frontBtn.textContent = 'Front Page';
frontBtn.style.display = stripActive ? 'inline-flex' : 'none';
const onionBtn = document.createElement('a');
onionBtn.id = 'eh-strip-onion-btn';
onionBtn.className = 'eh-strip-btn';
onionBtn.title = 'Tor / Onion Mirror';
onionBtn.href = cachedOnionUrl || '#';
onionBtn.textContent = 'Tor / Onion';
onionBtn.target = '_blank';
onionBtn.rel = 'noopener noreferrer';
onionBtn.style.display = stripActive && cachedOnionUrl ? 'inline-flex' : 'none';
const pageDisplay = document.createElement('span');
pageDisplay.id = 'eh-strip-page-display';
pageDisplay.className = 'eh-strip-page-counter';
pageDisplay.style.display = stripActive ? 'inline-block' : 'none';
controls.appendChild(modeBtn);
controls.appendChild(dirBtn);
controls.appendChild(galleryBtn);
controls.appendChild(frontBtn);
controls.appendChild(onionBtn);
controls.appendChild(pageDisplay);
const i2 = document.getElementById('i2');
if (i2) {
i2.appendChild(controls);
} else {
document.body.appendChild(controls);
}
}
placeStripControls();
updateToggleButtonUI();
syncPageIndicator(getCurrentlyVisiblePageNum());
}
window.addEventListener(
'mousemove',
(e) => {
if (!isReaderPage || !stripActive) return;
const i2 = document.getElementById('i2');
const bottom = i2 ? Math.max(50, i2.getBoundingClientRect().bottom) : 50;
if (e.clientY <= bottom) {
document.body.classList.add('eh-topbar-visible');
} else {
document.body.classList.remove('eh-topbar-visible');
}
},
{ passive: true }
);
document.addEventListener('mouseleave', () => {
if (!isReaderPage || !stripActive) return;
document.body.classList.remove('eh-topbar-visible');
if (document.activeElement && document.activeElement.blur) {
document.activeElement.blur();
}
});
window.addEventListener('blur', () => {
if (!isReaderPage || !stripActive) return;
document.body.classList.remove('eh-topbar-visible');
if (document.activeElement && document.activeElement.blur) {
document.activeElement.blur();
}
});
document.addEventListener('click', (e) => {
if (!isReaderPage || !stripActive) return;
const i2 = document.getElementById('i2');
if (i2 && i2.contains(e.target)) {
const btn = e.target.closest('button, a');
if (btn) {
setTimeout(() => { if (btn.blur) btn.blur(); }, 50);
}
} else if (i2 && e.clientY > 50) {
document.body.classList.remove('eh-topbar-visible');
}
}, { passive: true });
window.addEventListener('scroll', (e) => {
if (!isReaderPage || !stripActive) return;
scheduleScrollUpdate();
}, { passive: true });
window.addEventListener(
'wheel',
(e) => {
cancelKeyboardAnimation();
if (!isReaderPage || !stripActive || isTransitioningLayout) return;
const container = document.getElementById('eh-strip-container');
if (!container) return;
if (e.target && e.target.closest('#i2')) return;
if (currentLayout === 'horizontal') {
if (Math.abs(e.deltaY) >= Math.abs(e.deltaX)) {
e.preventDefault();
const rawDelta = e.deltaMode === 1 ? e.deltaY * 33 : (e.deltaMode === 2 ? e.deltaY * 100 : e.deltaY);
const clampedDelta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 100);
container.scrollLeft -= clampedDelta;
scheduleScrollUpdate();
}
}
},
{ passive: false }
);
let resizeTimer = null;
function handleViewportResize() {
if (!isReaderPage) return;
isTransitioningLayout = true;
cancelKeyboardAnimation();
if (resizeTimer) clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
instantScrollToPage(currentPageNum);
isTransitioningLayout = false;
scheduleScrollUpdate();
}, 70);
}
window.addEventListener('resize', handleViewportResize, { passive: true });
document.addEventListener('fullscreenchange', handleViewportResize, { passive: true });
document.addEventListener('webkitfullscreenchange', handleViewportResize, { passive: true });
function instantScrollToPage(targetPage) {
if (!isReaderPage) return;
const container = document.getElementById('eh-strip-container');
if (!container) return;
let item = pageItemMap.get(targetPage);
if (!item) item = createOrGetPlaceholder(targetPage);
if (!item) return;
cancelKeyboardAnimation();
if (currentLayout === 'horizontal') {
window.scrollTo(0, 0);
const itemRect = item.getBoundingClientRect();
const contRect = container.getBoundingClientRect();
const deltaX = ((itemRect.left + itemRect.right) / 2) - ((contRect.left + contRect.right) / 2);
const maxScroll = 0;
const minScroll = -(container.scrollWidth - container.clientWidth);
let targetLeft = container.scrollLeft + deltaX;
if (targetLeft > maxScroll) targetLeft = maxScroll;
if (targetLeft < minScroll) targetLeft = minScroll;
container.scrollLeft = targetLeft;
lastScrollLeft = targetLeft;
} else {
if (container) container.scrollLeft = 0;
const itemRect = item.getBoundingClientRect();
const deltaY = ((itemRect.top + itemRect.bottom) / 2) - (window.innerHeight / 2);
const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
let targetTop = window.scrollY + deltaY;
if (targetTop < 0) targetTop = 0;
if (targetTop > maxScroll) targetTop = maxScroll;
window.scrollTo(0, targetTop);
lastScrollY = targetTop;
}
}
function scrollToPage(targetPage) {
const total = getTotalPages();
if (total > 0) {
if (targetPage < 1) targetPage = 1;
if (targetPage > total) targetPage = total;
} else if (targetPage < 1) {
targetPage = 1;
}
currentPageNum = targetPage;
syncPageIndicator(targetPage);
updateTopBarInfo(targetPage);
checkAndLoadNearbyPages(targetPage);
let item = pageItemMap.get(targetPage);
if (!item) item = createOrGetPlaceholder(targetPage);
if (!item) return;
const container = document.getElementById('eh-strip-container');
if (!container) return;
cancelKeyboardAnimation();
if (currentLayout === 'horizontal') {
const itemRect = item.getBoundingClientRect();
const contRect = container.getBoundingClientRect();
const deltaX = ((itemRect.left + itemRect.right) / 2) - ((contRect.left + contRect.right) / 2);
const maxScroll = 0;
const minScroll = -(container.scrollWidth - container.clientWidth);
let targetLeft = container.scrollLeft + deltaX;
if (targetLeft > maxScroll) targetLeft = maxScroll;
if (targetLeft < minScroll) targetLeft = minScroll;
targetScrollLeft = targetLeft;
animateHorizontalScroll(container);
} else {
const itemRect = item.getBoundingClientRect();
const deltaY = ((itemRect.top + itemRect.bottom) / 2) - (window.innerHeight / 2);
const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
let targetTop = window.scrollY + deltaY;
if (targetTop < 0) targetTop = 0;
if (targetTop > maxScroll) targetTop = maxScroll;
targetScrollTop = targetTop;
animateVerticalScroll();
}
}
window.addEventListener(
'keydown',
(e) => {
const tag = document.activeElement?.tagName?.toLowerCase();
if (tag === 'input' || tag === 'textarea' || tag === 'select' || document.activeElement?.isContentEditable) {
return;
}
if (e.ctrlKey || e.altKey || e.metaKey) {
return;
}
if (isReaderPage) {
if (stripActive) {
const container = document.getElementById('eh-strip-container');
if (!container) return;
const isHoriz = currentLayout === 'horizontal';
const step = 140;
const pageStep = Math.round((isHoriz ? window.innerWidth : window.innerHeight) * 0.85);
if (e.key === '[' || e.key === '{') {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const now = Date.now();
if (now - lastStripTurnTime < 180) return;
lastStripTurnTime = now;
scrollDirection = 'backward';
scrollToPage(getCurrentlyVisiblePageNum() - 1);
return;
}
if (e.key === ']' || e.key === '}') {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const now = Date.now();
if (now - lastStripTurnTime < 180) return;
lastStripTurnTime = now;
scrollDirection = 'forward';
scrollToPage(getCurrentlyVisiblePageNum() + 1);
return;
}
if (e.key === 'Home') {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
cancelKeyboardAnimation();
if (isHoriz) {
targetScrollLeft = 0;
animateHorizontalScroll(container);
} else {
targetScrollTop = 0;
animateVerticalScroll();
}
return;
}
if (e.key === 'End') {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
cancelKeyboardAnimation();
if (isHoriz) {
targetScrollLeft = -(container.scrollWidth - container.clientWidth);
animateHorizontalScroll(container);
} else {
targetScrollTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
animateVerticalScroll();
}
return;
}
if (e.key === 'r' || e.key === 'R') {
e.preventDefault();
const visPage = getCurrentlyVisiblePageNum();
reloadPageImage(visPage, 0);
return;
}
let delta = 0;
if (
e.key === 'ArrowDown' || e.key === 's' || e.key === 'S' ||
e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A'
) {
delta = step;
} else if (
e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W' ||
e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D'
) {
delta = -step;
} else if (e.key === 'PageDown' || (e.key === ' ' && !e.shiftKey)) {
delta = pageStep;
} else if (e.key === 'PageUp' || (e.key === ' ' && e.shiftKey)) {
delta = -pageStep;
}
if (delta !== 0) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
scrollByEvenIncrement(delta);
}
return;
}
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const isRepeat = e.repeat;
const minInterval = isRepeat ? 300 : 100;
const now = Date.now();
if (now - lastSingleTurnTime < minInterval) return;
lastSingleTurnTime = now;
const targetP = currentPageNum + 1;
turnSinglePage(targetP, isRepeat);
} else if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const isRepeat = e.repeat;
const minInterval = isRepeat ? 300 : 100;
const now = Date.now();
if (now - lastSingleTurnTime < minInterval) return;
lastSingleTurnTime = now;
const targetP = currentPageNum - 1;
turnSinglePage(targetP, isRepeat);
} else if (e.key === 'ArrowDown' || e.key === 's' || e.key === 'S') {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
scrollByEvenIncrement(140);
} else if (e.key === 'ArrowUp' || e.key === 'w' || e.key === 'W') {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
scrollByEvenIncrement(-140);
} else if (e.key === 'PageDown' || (e.key === ' ' && !e.shiftKey)) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
scrollByEvenIncrement(Math.round(window.innerHeight * 0.85));
} else if (e.key === 'PageUp' || (e.key === ' ' && e.shiftKey)) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
scrollByEvenIncrement(-Math.round(window.innerHeight * 0.85));
} else if (e.key === 'Home') {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
targetScrollTop = 0;
animateVerticalScroll();
} else if (e.key === 'End') {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
targetScrollTop = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
animateVerticalScroll();
} else if (e.key === 'r' || e.key === 'R') {
e.preventDefault();
executeBrokenReload();
}
} else if (isGalleryPage) {
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
const nextThumb = document.querySelector('table.ptb td.ptds + td a, table.ptt td.ptds + td a') || document.querySelector('table.ptb td:last-child a');
if (nextThumb && nextThumb.href && nextThumb.textContent.trim() !== '<') {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (typeof window.__eh_navigateGalleryPage === 'function') {
window.__eh_navigateGalleryPage(nextThumb.href, true);
} else {
nextThumb.click();
}
}
} else if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') {
const curThumb = document.querySelector('table.ptb td.ptds, table.ptt td.ptds');
const prevThumb = curThumb?.previousElementSibling?.querySelector('a');
if (prevThumb && prevThumb.href) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (typeof window.__eh_navigateGalleryPage === 'function') {
window.__eh_navigateGalleryPage(prevThumb.href, true);
} else {
prevThumb.click();
}
}
}
}
},
true
);
window.addEventListener('popstate', (e) => {
if (isGalleryPage) {
if (typeof window.__eh_navigateGalleryPage === 'function') {
window.__eh_navigateGalleryPage(location.href, false);
}
return;
}
const targetMeta = getCurrentPageMeta();
const page = (e.state && e.state.page) || targetMeta.page;
if (page && page !== currentPageNum) {
if (stripActive) {
scrollToPage(page);
} else {
onPageTurn(page);
}
}
});
let isSinglePageTurning = false;
let pendingSinglePage = null;
function applySinglePageResponse(resp, targetPage) {
if (pendingSinglePage !== targetPage && currentPageNum !== targetPage) return;
const extracted = extractInfoFromResp(resp, targetPage);
if (extracted) pageTopBarInfoCache.set(targetPage, extracted);
if (window.history && window.history.pushState && resp && resp.s) {
const targetUrl = (window.base_url || '/') + resp.s;
window.history.pushState({ page: resp.p }, document.title, targetUrl);
}
window.apply_json_state(resp);
injectReaderToggle();
onPageTurn(targetPage);
isSinglePageTurning = false;
pendingSinglePage = null;
}
async function turnSinglePage(targetPage, isKeyRepeat = false, passedKey = null) {
if (stripActive) return;
const total = getTotalPages();
if (targetPage < 1 || (total > 0 && targetPage > total)) return;
if (isKeyRepeat && isSinglePageTurning) {
return;
}
isSinglePageTurning = true;
pendingSinglePage = targetPage;
currentPageNum = targetPage;
window.currentpage = targetPage;
if (passedKey) pageImgKeyCache.set(targetPage, passedKey);
// Hide previous image immediately and mark obsolete so monitors don't resurrect it
const oldImg = document.getElementById('img');
if (oldImg && oldImg._eh_page !== targetPage) {
oldImg.style.display = 'none';
oldImg._eh_obsolete = true;
oldImg.__eh_monitored = false;
}
showSinglePageThumbnailPlaceholder(targetPage);
syncPageIndicator(targetPage);
updateTopBarInfo(targetPage);
if (pageRespCache.has(targetPage)) {
const resp = pageRespCache.get(targetPage);
applySinglePageResponse(resp, targetPage);
return;
}
let key = passedKey || pageImgKeyCache.get(targetPage);
if (!key) {
key = await ensureKeysForPage(targetPage);
}
if (pendingSinglePage !== targetPage) {
return;
}
if (key) {
try {
const resp = await fetchApi(targetPage, key, null, false, true);
if (pendingSinglePage !== targetPage) return;
if (resp && !resp.error) {
pageRespCache.set(targetPage, resp);
extractKeysFromResp(resp, targetPage);
applySinglePageResponse(resp, targetPage);
} else if (typeof window.origLoadImage === 'function') {
window.origLoadImage(targetPage, key);
}
} finally {
if (pendingSinglePage === targetPage) {
isSinglePageTurning = false;
pendingSinglePage = null;
}
}
} else {
const nextOrPrev = targetPage > (window.currentpage || 1) ? document.getElementById('next') : document.getElementById('prev');
if (nextOrPrev) nextOrPrev.click();
isSinglePageTurning = false;
pendingSinglePage = null;
}
}
function hookReader() {
if (typeof window.load_image === 'function' && !window.load_image.__eh_hooked) {
const origLoadImage = window.load_image;
window.origLoadImage = origLoadImage;
window.load_image = function (page, imgKey) {
currentPageNum = page;
window.currentpage = page;
if (imgKey) window.currentkey = imgKey;
if (!stripActive) {
turnSinglePage(page, false, imgKey);
return false;
}
if (pageRespCache.has(page)) {
const resp = pageRespCache.get(page);
if (window.history && window.history.pushState) {
const targetUrl = (window.base_url || '/') + resp.s;
window.history.pushState({ page: resp.p }, document.title, targetUrl);
}
window.apply_json_state(resp);
injectReaderToggle();
onPageTurn(page);
return false;
}
return origLoadImage.apply(this, arguments);
};
window.load_image.__eh_hooked = true;
}
if (typeof window.apply_json_state === 'function' && !window.apply_json_state.__eh_hooked) {
const origApply = window.apply_json_state;
window.apply_json_state = function () {
const ret = origApply.apply(this, arguments);
const activeP = (arguments[0] && arguments[0].p) || currentPageNum;
currentPageNum = activeP;
window.currentpage = activeP;
const resp = arguments[0];
if (resp) {
extractKeysFromResp(resp, activeP);
const newNl = extractNlCode(resp);
if (newNl) pageNlCodes.set(activeP, newNl);
}
if (!stripActive) {
const newImg = document.getElementById('img');
if (newImg) {
newImg._eh_page = activeP;
newImg._eh_obsolete = false;
newImg.__eh_monitored = false;
const isPreloaded = preloadedImages.has(activeP) && preloadedImages.get(activeP)?.isReady;
const isImgComplete = newImg.complete && newImg.naturalWidth > 0;
if (isPreloaded || isImgComplete) {
newImg.style.display = 'block';
hideSinglePageThumbnailPlaceholder();
} else {
newImg.style.display = 'none';
showSinglePageThumbnailPlaceholder(activeP);
const onImgLoaded = async () => {
newImg.removeEventListener('load', onImgLoaded);
newImg.removeEventListener('error', onImgError);
if (newImg._eh_page !== currentPageNum || newImg._eh_obsolete) return;
try {
if ('decode' in newImg) await newImg.decode();
} catch {}
if (newImg._eh_page !== currentPageNum || newImg._eh_obsolete) return;
newImg.style.display = 'block';
hideSinglePageThumbnailPlaceholder();
};
const onImgError = () => {
newImg.removeEventListener('load', onImgLoaded);
newImg.removeEventListener('error', onImgError);
if (newImg._eh_page !== currentPageNum || newImg._eh_obsolete) return;
handleBrokenImage('load error');
};
newImg.addEventListener('load', onImgLoaded, { once: true });
newImg.addEventListener('error', onImgError, { once: true });
}
}
}
injectReaderToggle();
onPageTurn(activeP);
return ret;
};
window.apply_json_state.__eh_hooked = true;
}
if (typeof window.api_response === 'function' && !window.api_response.__eh_safe) {
const origApiResponse = window.api_response;
window.api_response = function (xhr) {
try {
if (xhr && typeof xhr.responseText === 'string') {
const parsed = safeParseJson(xhr.responseText);
if (parsed) return parsed;
return { error: xhr.responseText.trim() || 'Invalid response' };
}
return origApiResponse.apply(this, arguments);
} catch {
return { error: 'Invalid JSON response' };
}
};
window.api_response.__eh_safe = true;
}
}
let turnDebounce = null;
function onPageTurn(activePage) {
if (turnDebounce) clearTimeout(turnDebounce);
turnDebounce = setTimeout(() => {
injectReaderToggle();
syncThemeClass();
pruneCaches(activePage || currentPageNum);
initImageMonitor();
startPrefetch();
if (!stripActive) {
const img = document.getElementById('img');
const targetP = activePage || currentPageNum;
if (img && img._eh_page === targetP && !img._eh_obsolete && img.complete && img.naturalWidth > 0) {
img.style.display = 'block';
hideSinglePageThumbnailPlaceholder();
} else {
showSinglePageThumbnailPlaceholder(targetP);
}
const anchor = document.getElementById('i1') || document.getElementById('i3');
if (anchor) {
const y = anchor.getBoundingClientRect().top + window.scrollY - 10;
if (window.scrollY > y) {
window.scrollTo(0, Math.max(0, y));
}
}
}
}, 25);
}
function clearStallTimer() {
if (stallTimer) {
clearTimeout(stallTimer);
stallTimer = null;
}
}
async function bounceSinglePageImage(pageNo) {
if (isReloading) return;
isReloading = true;
clearStallTimer();
const pageKey = 'eh_retry_' + location.pathname + '_' + pageNo;
const count = parseInt(safeStorageGet(sessionStorage, pageKey, '0'), 10);
if (count >= MAX_RETRIES) {
showFailureToast(`⚠️ Image for page ${pageNo} failed after ${MAX_RETRIES} attempts. Click to retry.`);
isReloading = false;
return;
}
safeStorageSet(sessionStorage, pageKey, String(count + 1));
showSinglePageThumbnailPlaceholder(pageNo);
const nl = pageNlCodes.get(pageNo) || extractNlCode(document);
let imgKey = pageImgKeyCache.get(pageNo);
if (!imgKey) {
imgKey = await ensureKeysForPage(pageNo);
}
if (imgKey) {
try {
const freshResp = await fetchApi(pageNo, imgKey, nl || null, true);
if (freshResp) {
pageRespCache.set(pageNo, freshResp);
extractKeysFromResp(freshResp, pageNo);
const newNl = extractNlCode(freshResp);
if (newNl) {
pageNlCodes.set(pageNo, newNl);
}
const extracted = extractInfoFromResp(freshResp, pageNo);
if (extracted) pageTopBarInfoCache.set(pageNo, extracted);
updateTopBarInfo(pageNo);
if (typeof window.apply_json_state === 'function') {
window.apply_json_state(freshResp);
injectReaderToggle();
const newImg = document.getElementById('img');
if (newImg) {
newImg.__eh_monitored = false;
}
initImageMonitor();
isReloading = false;
return;
}
}
} catch (err) {
console.warn('ExFastReader: In-place bounce failed, trying fallback:', err);
}
}
isReloading = false;
const { gid } = getPageParams();
if (imgKey && gid && nl) {
location.href = `${location.origin}/s/${imgKey}/${gid}-${pageNo}?nl=${encodeURIComponent(nl)}`;
} else {
const loadfail = document.getElementById('loadfail');
if (loadfail) loadfail.click();
else location.reload();
}
}
function handleBrokenImage(reason) {
bounceSinglePageImage(currentPageNum);
}
function executeBrokenReload() {
bounceSinglePageImage(currentPageNum);
}
function initImageMonitor() {
if (stripActive) return;
isReloading = false;
clearStallTimer();
const img = document.getElementById('img');
if (!img) return;
if (img._eh_page !== undefined && img._eh_page !== currentPageNum) return;
if (img._eh_obsolete) return;
if (img.__eh_monitored) return;
img.__eh_monitored = true;
const pageNo = currentPageNum;
if (img._eh_page === undefined) {
img._eh_page = pageNo;
}
let bounceTriggered = false;
const startTime = Date.now();
const triggerBounce = (reason) => {
if (bounceTriggered || isReloading) return;
bounceTriggered = true;
clearStallTimer();
img.removeEventListener('error', onError);
img.removeEventListener('load', onLoad);
console.warn(`ExFastReader: Single page ${pageNo} image ${reason}. Triggering in-place failover bounce.`);
bounceSinglePageImage(pageNo);
};
const onError = () => {
if (img._eh_page !== currentPageNum || img._eh_obsolete) return;
triggerBounce('network load error');
};
const onLoad = () => {
clearStallTimer();
img.removeEventListener('error', onError);
img.removeEventListener('load', onLoad);
if (img._eh_page !== currentPageNum || img._eh_obsolete) return;
if (img.naturalWidth === 0 || img.naturalHeight === 0) {
triggerBounce('0x0 dimension after load');
} else {
img.style.display = 'block';
hideSinglePageThumbnailPlaceholder();
safeStorageRemove(sessionStorage, 'eh_retry_' + location.pathname + '_' + pageNo);
const durationSec = Math.max(0.1, (Date.now() - startTime) / 1000);
recordDownloadSpeedSample(pageNo, durationSec);
}
};
if (img.complete) {
if (img.naturalWidth === 0 || img.naturalHeight === 0) {
triggerBounce('0x0 dimension on arrival');
return;
}
img.style.display = 'block';
hideSinglePageThumbnailPlaceholder();
safeStorageRemove(sessionStorage, 'eh_retry_' + location.pathname + '_' + pageNo);
} else {
img.addEventListener('error', onError, { once: true });
img.addEventListener('load', onLoad, { once: true });
const { slowTimeoutMs } = calculateImageTimeout(pageNo);
stallTimer = setTimeout(() => {
if (img._eh_page !== currentPageNum || img._eh_obsolete) return;
if (!img.complete || img.naturalWidth === 0) {
triggerBounce(`slow / stalled (> ${(slowTimeoutMs / 1000).toFixed(1)}s)`);
}
}, slowTimeoutMs);
}
}
function initObservers() {
const i2 = document.getElementById('i2');
if (i2 && !i2.__eh_observed) {
i2.__eh_observed = true;
const obs = new MutationObserver(() => {
if (!document.getElementById('eh-strip-controls')) {
injectReaderToggle();
}
});
obs.observe(i2, { childList: true });
}
const i1 = document.getElementById('i1') || document.body;
if (i1 && !i1.__eh_observed) {
i1.__eh_observed = true;
const obs = new MutationObserver(() => {
if (!stripActive) {
placeStripControls();
}
});
obs.observe(i1, { childList: true });
}
const container = document.getElementById('i3');
if (container && !container.__eh_observed) {
container.__eh_observed = true;
let obsDebounce = null;
const obs = new MutationObserver(() => {
if (obsDebounce) clearTimeout(obsDebounce);
obsDebounce = setTimeout(() => {
if (!stripActive) initImageMonitor();
}, 20);
});
obs.observe(container, { childList: true });
}
}
// Watchdog to ensure no page stays stuck in loadingPages permanently
setInterval(() => {
if (!isReaderPage || loadingPages.size === 0) return;
const now = Date.now();
let hadStuck = false;
for (const [p, startTime] of loadingPages.entries()) {
const { watchdogTimeoutMs } = calculateImageTimeout(p);
if (now - startTime > watchdogTimeoutMs) {
console.warn(`ExFastReader Watchdog: Page ${p} exceeded ${(watchdogTimeoutMs / 1000).toFixed(1)}s load limit. Force releasing concurrency slot.`);
loadingPages.delete(p);
hadStuck = true;
const item = pageItemMap.get(p);
if (item) {
item.classList.remove('eh-loading');
if (typeof item._abortImg === 'function') {
item._abortImg();
item._abortImg = null;
} else if (item._currentImgToken) {
const stuckImg = item._currentImgToken;
stuckImg.onload = null;
stuckImg.onerror = null;
stuckImg.removeAttribute('src');
stuckImg.src = '';
}
item._currentImgToken = null;
if (!item.querySelector('img.eh-main-img')) {
recordPageFailure(p, `Watchdog load timeout > ${(watchdogTimeoutMs / 1000).toFixed(1)}s`);
const failInfo = failedPages.get(p);
if (failInfo && failInfo.count >= MAX_RETRIES) {
showRetryUI(item, p);
} else {
reloadPageImage(p, failInfo ? failInfo.count : 1);
}
}
}
}
}
if (hadStuck) {
checkAndLoadNearbyPages();
}
}, 3000);
function initIndexInfiniteScroll() {
if (!isIndexPage) return;
let infiniteScrollActive = safeStorageGet(localStorage, 'eh_infinite_scroll_enabled', '1') !== '0';
let isLoadingNext = false;
let nextPageUrl = getNextPageUrl(document);
const loadedUrls = new Set([location.href]);
let sentinelObserver = null;
function findThumbnailDropdown() {
let sel = document.querySelector("select[onchange*='inline_set=dm_']");
if (sel && !sel.closest('#favact, .favact, [id*="favact"]')) return sel;
sel = document.querySelector("select[onchange*='inline_set'], select[name*='inline_set']");
if (sel && !sel.closest('#favact, .favact, [id*="favact"]')) return sel;
const allSelects = document.querySelectorAll('select');
for (const s of allSelects) {
if (s.closest('#favact, .favact, [id*="favact"]') || s.name === 'favact' || s.id === 'favsel' || s.name === 'ddact') continue;
const oc = s.getAttribute('onchange') || '';
if (oc.includes('inline_set') || oc.includes('dm_')) return s;
for (const opt of s.options) {
const txt = (opt.textContent || '').trim().toLowerCase();
if (txt === 'thumbnail' || txt === 'thumbnails' || opt.value === 't' || opt.value === 'dm_t') {
return s;
}
}
}
return null;
}
function getFavoritesForm() {
const sampleCb = document.querySelector('.itg input[type="checkbox"], input[name^="modifygals"]');
if (sampleCb) {
const f = sampleCb.closest('form');
if (f) return f;
}
const itg = document.querySelector('.itg');
if (itg) {
const f = itg.closest('form');
if (f) return f;
}
const postForm = document.querySelector('form[method="post"][action*="favorites"], form[method="POST"][action*="favorites"], form[method="post"]');
if (postForm) return postForm;
const favform = document.getElementById('favform') || document.querySelector('form[name="favform"]');
if (favform && (!favform.method || favform.method.toLowerCase() === 'post')) return favform;
return null;
}
function getFavactElement() {
const byId = document.getElementById('favact');
if (byId && byId.id !== 'eh-favact-top' && byId.tagName.toLowerCase() !== 'select' && byId.tagName.toLowerCase() !== 'input') {
return byId;
}
const byClass = document.querySelector('div.favact:not(.eh-favact-top), p.favact:not(.eh-favact-top)');
if (byClass) return byClass;
const inputEl = document.querySelector('select[name="favact"], select[name="ddact"], input[name="apply"], select#favsel');
if (inputEl) {
const parent = inputEl.closest('#favact, .favact, div, p');
if (parent && parent !== document.body && parent !== document.documentElement && !parent.classList.contains('eh-favact-top') && parent.id !== 'eh-favact-top') {
return parent;
}
}
return byId || null;
}
function toggleAllGalleries(checked) {
if (typeof window.toggle_all === 'function') {
try {
window.toggle_all(checked);
} catch (err) {
console.warn('ExFastReader: native toggle_all error', err);
}
}
const galleryCheckboxes = document.querySelectorAll(
'.itg input[type="checkbox"], input[name^="modifygals"], input[name*="modifygals"]'
);
galleryCheckboxes.forEach((cb) => {
cb.checked = checked;
});
document.querySelectorAll(
'#alltoggle, [name="alltoggle"], .alltoggle, #eh-alltoggle-top, input[type="checkbox"][onclick*="toggle_all"]'
).forEach((cb) => {
cb.checked = checked;
});
}
function executeFavAction(selectedAction) {
const form = getFavoritesForm();
if (!form) return;
if (!form.id) form.id = 'favform';
let ddactInput = form.querySelector('select[name="ddact"], select[name="favact"]');
if (ddactInput) {
ddactInput.value = selectedAction;
} else {
let hiddenAct = form.querySelector('input[name="ddact"].eh-sync-input');
if (!hiddenAct) {
hiddenAct = document.createElement('input');
hiddenAct.type = 'hidden';
hiddenAct.name = 'ddact';
hiddenAct.className = 'eh-sync-input';
form.appendChild(hiddenAct);
}
hiddenAct.value = selectedAction;
}
let hiddenApply = form.querySelector('input[name="apply"].eh-sync-input');
if (!hiddenApply) {
hiddenApply = document.createElement('input');
hiddenApply.type = 'hidden';
hiddenApply.name = 'apply';
hiddenApply.value = 'Apply';
hiddenApply.className = 'eh-sync-input';
form.appendChild(hiddenApply);
}
const checkedBoxes = document.querySelectorAll(
'.itg input[type="checkbox"]:checked, input[name^="modifygals"]:checked'
);
checkedBoxes.forEach((cb) => {
if (cb.form !== form) {
cb.setAttribute('form', form.id);
}
});
if (typeof form.requestSubmit === 'function') {
form.requestSubmit();
} else {
form.submit();
}
}
function getNextPageUrl(doc = document) {
const ptdsNext = doc.querySelector('table.ptb td.ptds + td a, table.ptt td.ptds + td a');
if (ptdsNext && ptdsNext.href && ptdsNext.textContent.trim() !== '<') {
return ptdsNext.href;
}
const searchnavNext = doc.querySelector('.searchnav a[href*="next="], .searchnav a#next');
if (searchnavNext && searchnavNext.href) {
return searchnavNext.href;
}
const lastA = doc.querySelector('table.ptb td:last-child a, table.ptt td:last-child a');
if (lastA && lastA.href && (lastA.textContent.trim() === '>' || lastA.textContent.trim() === '»')) {
return lastA.href;
}
const relNext = doc.querySelector('a[rel="next"]');
if (relNext && relNext.href) {
return relNext.href;
}
return null;
}
function getOrCreateSentinel() {
let sentinel = document.getElementById('eh-infinite-sentinel');
if (!sentinel) {
sentinel = document.createElement('div');
sentinel.id = 'eh-infinite-sentinel';
sentinel.className = 'eh-infinite-sentinel';
sentinel.style.cssText = 'text-align: center; padding: 18px 0; margin: 12px auto; font-family: arial, helvetica, sans-serif; font-size: 10pt; font-weight: bold; width: 100%; clear: both; user-select: none;';
const itg = document.querySelector('.itg');
const ptb = document.querySelector('table.ptb, .itg + .searchnav, #favform + .searchnav');
if (itg && itg.parentNode) {
if (ptb && ptb.parentNode === itg.parentNode) {
itg.parentNode.insertBefore(sentinel, ptb);
} else if (itg.nextSibling) {
itg.parentNode.insertBefore(sentinel, itg.nextSibling);
} else {
itg.parentNode.appendChild(sentinel);
}
}
}
return sentinel;
}
function updateSentinelState(state) {
const sentinel = getOrCreateSentinel();
if (!sentinel) return;
if (!infiniteScrollActive) {
sentinel.style.display = 'none';
return;
}
sentinel.style.display = 'block';
if (state === 'loading') {
sentinel.innerHTML = `
<div style="display: inline-flex; align-items: center; justify-content: center; gap: 8px;">
<span class="eh-spinner" style="display: inline-block;"></span>
<span>Loading next page...</span>
</div>
`;
} else if (state === 'end') {
sentinel.innerHTML = `<span style="opacity: 0.7;">— All pages loaded —</span>`;
} else if (state === 'error') {
sentinel.innerHTML = `
<span style="color: #ef4444; margin-right: 8px;">Failed to load next page.</span>
<button type="button" class="eh-strip-btn active" id="eh-infinite-retry-btn">Retry</button>
`;
const btn = document.getElementById('eh-infinite-retry-btn');
if (btn) {
btn.onclick = () => loadNextPage();
}
} else {
sentinel.innerHTML = '';
}
}
async function loadNextPage() {
if (!infiniteScrollActive || isLoadingNext || !nextPageUrl || loadedUrls.has(nextPageUrl)) return;
isLoadingNext = true;
updateSentinelState('loading');
const fetchUrl = nextPageUrl;
loadedUrls.add(fetchUrl);
try {
const res = await fetch(fetchUrl, { credentials: 'include' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const currentItg = document.querySelector('.itg');
const newItg = doc.querySelector('.itg');
if (currentItg && newItg) {
if (currentItg.tagName.toLowerCase() === 'table') {
const currentTbody = currentItg.querySelector(':scope > tbody') || currentItg;
const newTbody = newItg.querySelector(':scope > tbody') || newItg;
const newRows = Array.from(newTbody.children).filter(
el => el.tagName && el.tagName.toLowerCase() === 'tr' && !el.querySelector('th')
);
for (const row of newRows) {
currentTbody.appendChild(document.importNode(row, true));
}
} else {
const newItems = Array.from(newItg.children);
for (const item of newItems) {
currentItg.appendChild(document.importNode(item, true));
}
}
}
const curPtb = document.querySelector('table.ptb');
const newPtb = doc.querySelector('table.ptb');
if (curPtb && newPtb) {
curPtb.innerHTML = newPtb.innerHTML;
}
nextPageUrl = getNextPageUrl(doc);
syncFavactPosition();
if (nextPageUrl && !loadedUrls.has(nextPageUrl)) {
updateSentinelState('idle');
} else {
nextPageUrl = null;
updateSentinelState('end');
}
} catch (err) {
console.warn('ExFastReader: Infinite scroll page load error', err);
loadedUrls.delete(fetchUrl);
updateSentinelState('error');
} finally {
isLoadingNext = false;
}
}
function setupObserver() {
if (sentinelObserver) {
sentinelObserver.disconnect();
sentinelObserver = null;
}
if (!infiniteScrollActive) return;
const sentinel = getOrCreateSentinel();
if (!sentinel) return;
if (typeof IntersectionObserver !== 'undefined') {
sentinelObserver = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
loadNextPage();
}
}
}, { rootMargin: '600px' });
sentinelObserver.observe(sentinel);
}
}
let isSyncingFavact = false;
function syncFavactPosition() {
if (!location.pathname.startsWith('/favorites')) return;
if (isSyncingFavact) return;
isSyncingFavact = true;
try {
const bottomFavact = getFavactElement();
const ido = document.querySelector('.ido') || document.querySelector('div.ido');
if (!ido) return;
if (ido.style.position !== 'relative') {
ido.style.setProperty('position', 'relative', 'important');
}
let topFavact = document.getElementById('eh-favact-top');
if (infiniteScrollActive) {
if (!topFavact && bottomFavact) {
topFavact = bottomFavact.cloneNode(true);
topFavact.id = 'eh-favact-top';
topFavact.classList.add('eh-favact-top');
const cloneCheckbox = topFavact.querySelector('#alltoggle, [name="alltoggle"]');
if (cloneCheckbox) cloneCheckbox.id = 'eh-alltoggle-top';
ido.insertBefore(topFavact, ido.firstChild);
} else if (topFavact) {
topFavact.style.display = 'inline-flex';
}
const topSelect = topFavact ? topFavact.querySelector('select') : null;
const bottomSelect = bottomFavact ? bottomFavact.querySelector('select') : null;
const topApply = topFavact ? topFavact.querySelector('input[type="submit"], input[type="button"], button') : null;
const bottomApply = bottomFavact ? bottomFavact.querySelector('input[type="submit"], input[type="button"], button') : null;
// Two-way dropdown sync
if (topSelect && !topSelect.__eh_bound) {
topSelect.__eh_bound = true;
topSelect.addEventListener('change', () => {
if (bottomSelect) bottomSelect.value = topSelect.value;
});
}
if (bottomSelect && !bottomSelect.__eh_bound) {
bottomSelect.__eh_bound = true;
bottomSelect.addEventListener('change', () => {
if (topSelect) topSelect.value = bottomSelect.value;
});
}
// Apply button handling
if (topApply && !topApply.__eh_bound) {
topApply.__eh_bound = true;
topApply.addEventListener('click', (e) => {
e.preventDefault();
executeFavAction(topSelect ? topSelect.value : (bottomSelect ? bottomSelect.value : ''));
});
}
if (bottomApply && !bottomApply.__eh_bound) {
bottomApply.__eh_bound = true;
bottomApply.addEventListener('click', (e) => {
e.preventDefault();
executeFavAction(bottomSelect ? bottomSelect.value : (topSelect ? topSelect.value : ''));
});
}
} else {
if (topFavact) {
topFavact.style.display = 'none';
}
}
} catch (err) {
console.warn('ExFastReader: syncFavactPosition error', err);
} finally {
isSyncingFavact = false;
}
} // Delegated listener for alltoggle checkboxes (both top and bottom)
document.addEventListener('click', (e) => {
const toggleCb = e.target.closest('#alltoggle, [name="alltoggle"], .alltoggle, #eh-alltoggle-top, input[type="checkbox"][onclick*="toggle_all"]');
if (toggleCb && toggleCb.type === 'checkbox') {
setTimeout(() => {
toggleAllGalleries(toggleCb.checked);
}, 0);
}
});
// Delegated listener for individual gallery checkboxes to keep master checkboxes synced
document.addEventListener('change', (e) => {
if (e.target && e.target.type === 'checkbox') {
if (e.target.matches('.itg input[type="checkbox"], input[name^="modifygals"], input[name*="modifygals"]')) {
const all = document.querySelectorAll('.itg input[type="checkbox"], input[name^="modifygals"], input[name*="modifygals"]');
const checked = document.querySelectorAll('.itg input[type="checkbox"]:checked, input[name^="modifygals"]:checked, input[name*="modifygals"]:checked');
const isAll = all.length > 0 && all.length === checked.length;
document.querySelectorAll('#alltoggle, [name="alltoggle"], .alltoggle, #eh-alltoggle-top').forEach((cb) => {
cb.checked = isAll;
});
}
}
}, { passive: true });
function toggleInfiniteScroll() {
infiniteScrollActive = !infiniteScrollActive;
safeStorageSet(localStorage, 'eh_infinite_scroll_enabled', infiniteScrollActive ? '1' : '0');
syncFavactPosition();
const btn = document.getElementById('eh-infinite-scroll-toggle');
if (btn) {
btn.classList.toggle('active', infiniteScrollActive);
}
if (infiniteScrollActive) {
updateSentinelState('idle');
setupObserver();
const scrollDist = document.documentElement.scrollHeight - (window.scrollY + window.innerHeight);
if (scrollDist < 800) {
loadNextPage();
}
} else {
updateSentinelState('idle');
if (sentinelObserver) {
sentinelObserver.disconnect();
sentinelObserver = null;
}
}
}
function injectToggle() {
const dropdown = findThumbnailDropdown();
if (!dropdown || document.getElementById('eh-infinite-scroll-toggle')) return Boolean(dropdown);
const wrap = document.createElement('span');
wrap.id = 'eh-infinite-scroll-wrap';
wrap.style.cssText = 'display: inline-flex; align-items: center; margin-left: 8px; vertical-align: middle; height: 100%;';
const btn = document.createElement('button');
btn.type = 'button';
btn.id = 'eh-infinite-scroll-toggle';
btn.className = 'eh-strip-btn' + (infiniteScrollActive ? ' active' : '');
btn.title = 'Toggle Infinite Scrolling';
btn.textContent = 'Infinite Scroll';
const syncDropdownStyle = () => {
if (!dropdown.isConnected || !btn.isConnected) return;
const cs = window.getComputedStyle(dropdown);
if (cs.fontFamily) btn.style.fontFamily = cs.fontFamily;
if (cs.fontSize) btn.style.fontSize = cs.fontSize;
if (cs.fontWeight) btn.style.fontWeight = cs.fontWeight;
const h = dropdown.offsetHeight;
if (h > 0) {
btn.style.height = h + 'px';
btn.style.boxSizing = 'border-box';
}
};
btn.addEventListener('click', (e) => {
e.preventDefault();
toggleInfiniteScroll();
btn.blur();
});
wrap.appendChild(btn);
dropdown.insertAdjacentElement('afterend', wrap);
syncDropdownStyle();
requestAnimationFrame(syncDropdownStyle);
setTimeout(syncDropdownStyle, 100);
if (infiniteScrollActive) {
setupObserver();
}
syncFavactPosition();
return true;
}
if (location.pathname.startsWith('/favorites')) {
syncFavactPosition();
let favDebounce = null;
const favObs = new MutationObserver(() => {
if (favDebounce) return;
favDebounce = setTimeout(() => {
favDebounce = null;
syncFavactPosition();
}, 80);
});
favObs.observe(document.body || document.documentElement, { childList: true, subtree: true });
setTimeout(() => favObs.disconnect(), 15000);
}
window.addEventListener('scroll', () => {
if (!infiniteScrollActive || isLoadingNext || !nextPageUrl) return;
const scrollDist = document.documentElement.scrollHeight - (window.scrollY + window.innerHeight);
if (scrollDist < 800) {
loadNextPage();
}
}, { passive: true });
let injected = injectToggle();
if (!injected) {
let obsTimeout = null;
const obs = new MutationObserver(() => {
if (injectToggle()) {
if (obsTimeout) clearTimeout(obsTimeout);
obs.disconnect();
}
});
obs.observe(document.documentElement, { childList: true, subtree: true });
obsTimeout = setTimeout(() => obs.disconnect(), 10000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
if (!nextPageUrl) nextPageUrl = getNextPageUrl(document);
syncFavactPosition();
if (infiniteScrollActive) setupObserver();
if (!document.getElementById('eh-infinite-scroll-toggle')) injectToggle();
}, { once: true });
}
}
syncThemeClass();
const initialMeta = getCurrentPageMeta();
currentPageNum = initialMeta.page;
if (initialMeta.imgKey) {
pageImgKeyCache.set(currentPageNum, initialMeta.imgKey);
}
const initialImg = document.getElementById('img');
if (initialImg) {
initialImg._eh_page = currentPageNum;
initialImg._eh_obsolete = false;
}
if (isReaderPage) {
let pollCount = 0;
const hookTimer = setInterval(() => {
pollCount++;
const { gid, showkey } = getPageParams();
const i2 = document.getElementById('i2');
if (i2) {
injectReaderToggle();
syncThemeClass();
initObservers();
}
const canStart = typeof window.load_image === 'function' &&
typeof window.apply_json_state === 'function' &&
gid && showkey &&
(i2 || document.readyState !== 'loading' || pollCount > 60);
if (canStart) {
hookReader();
detectInitialThumbsPerPage();
discoverAndFetchGallery(currentPageNum);
prefetchNearbyThumbnails(currentPageNum);
if (stripActive) {
enableStripMode();
} else {
initImageMonitor();
startPrefetch();
injectReaderToggle();
}
clearInterval(hookTimer);
} else if (pollCount > 400) {
clearInterval(hookTimer);
}
}, 25);
} else if (isGalleryPage) {
let lastFavToggleTime = 0;
let cachedPopupData = null; // { note: '', slots: '', names: [], popupItems: [], isFavorited: bool, favcat: num }
let savedFavLinkInitialHtml = null;
const DEFAULT_FAV_NAMES = [
'Favorites 0', 'Favorites 1', 'Favorites 2', 'Favorites 3', 'Favorites 4',
'Favorites 5', 'Favorites 6', 'Favorites 7', 'Favorites 8', 'Favorites 9'
];
function getSavedFavNames() {
try {
const raw = safeStorageGet(localStorage, 'eh_fav_cat_names', null);
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed) && parsed.length >= 10) return parsed;
}
} catch {}
return [...DEFAULT_FAV_NAMES];
}
function detectCurrentCategory() {
if (cachedPopupData && typeof cachedPopupData.favcat === 'number' && cachedPopupData.isFavorited) {
return cachedPopupData.favcat;
}
const favDiv = document.getElementById('fav');
if (favDiv) {
const style = (favDiv.getAttribute('style') || '') + ' ' + (favDiv.style.cssText || '') + ' ' + (favDiv.style.backgroundPosition || '');
const m = style.match(/(?:0(?:px|%)?|left)\s+(-?\d+)px/i) ||
style.match(/background-position:\s*[^;]*?(-?\d+)px/i) ||
style.match(/(-?\d+)px\s+no-repeat/i) ||
style.match(/0px\s+(-?\d+)px/i);
if (m) {
const posY = parseInt(m[1], 10);
const idx = Math.round((-2 - posY) / 19);
if (idx >= 0 && idx <= 9) return idx;
}
const title = favDiv.getAttribute('title');
if (title && title.trim()) {
const names = getSavedFavNames();
const idx = names.findIndex(n => n.toLowerCase() === title.trim().toLowerCase());
if (idx !== -1) return idx;
const mNum = title.match(/Favorites\s+(\d)/i);
if (mNum) {
const catNum = parseInt(mNum[1], 10);
if (catNum >= 0 && catNum <= 9) return catNum;
}
}
}
const favLink = document.getElementById('favoritelink');
if (favLink) {
const txt = favLink.textContent.trim().toLowerCase();
if (txt && !txt.includes('add to')) {
const names = getSavedFavNames();
const idx = names.findIndex(n => n.toLowerCase() === txt);
if (idx !== -1) return idx;
const mNum = txt.match(/favorites\s+(\d)/i);
if (mNum) {
const catNum = parseInt(mNum[1], 10);
if (catNum >= 0 && catNum <= 9) return catNum;
}
}
}
return null;
}
function isGalleryFavorited() {
if (cachedPopupData && typeof cachedPopupData.isFavorited === 'boolean') {
return cachedPopupData.isFavorited;
}
const favLink = document.getElementById('favoritelink');
if (favLink) {
const txt = favLink.textContent.trim().toLowerCase();
if (txt.includes('edit')) return true;
if (txt && !txt.includes('add to')) return true;
}
const favDiv = document.getElementById('fav');
if (favDiv) {
if (favDiv.getAttribute('title')) return true;
const style = (favDiv.getAttribute('style') || '') + ' ' + (favDiv.style.cssText || '');
if (style.includes('fav.png') || /background(-image)?:\s*url/i.test(style)) return true;
}
return detectCurrentCategory() !== null;
}
// Intercept mousedown, pointerdown, and click on window to prevent foreign scripts (ComicRead, etc.) or native popup
['mousedown', 'pointerdown', 'click'].forEach((evtType) => {
window.addEventListener(evtType, (e) => {
const favTarget = e.target.closest('#gdf, #favoritelink, [onclick*="pop_fav"], [onclick*="addfav"]');
if (favTarget) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (evtType === 'click') {
openInlineFavorites();
}
return false;
}
}, true);
});
// Override pop_fav, popUp, window.open
try {
const _popFavOverride = function () {
openInlineFavorites();
return false;
};
Object.defineProperty(window, 'pop_fav', {
configurable: true,
enumerable: true,
get() { return _popFavOverride; },
set() {}
});
} catch {
window.pop_fav = function () { openInlineFavorites(); return false; };
}
const _origPopUp = window.popUp;
window.popUp = function (url, w, h) {
if (typeof url === 'string' && (url.includes('addfav') || url.includes('act=addfav'))) {
openInlineFavorites();
return false;
}
if (typeof _origPopUp === 'function') {
return _origPopUp.apply(this, arguments);
}
return false;
};
const _origWindowOpen = window.open;
window.open = function (url, name, specs) {
if (typeof url === 'string' && (url.includes('addfav') || url.includes('act=addfav'))) {
openInlineFavorites();
return null;
}
if (typeof _origWindowOpen === 'function') {
return _origWindowOpen.apply(this, arguments);
}
return null;
};
function purgeForeignPopups() {
const gd3 = document.getElementById('gd3');
if (gd3) {
Array.from(gd3.children).forEach((child) => {
const id = child.id;
const cls = child.className;
if (!['gdc', 'gdn', 'gdd', 'gdr', 'gdf'].includes(id) && cls !== 'c') {
child.remove();
}
});
}
}
function sanitizeFavElements() {
const oldGdf = document.getElementById('gdf');
if (oldGdf && !oldGdf.__eh_sanitized) {
const favLink = oldGdf.querySelector('#favoritelink');
if (favLink && !savedFavLinkInitialHtml && favLink.querySelector('img')) {
savedFavLinkInitialHtml = favLink.innerHTML;
}
const newGdf = oldGdf.cloneNode(true);
newGdf.__eh_sanitized = true;
newGdf.removeAttribute('onclick');
const newFavLink = newGdf.querySelector('#favoritelink');
if (newFavLink) newFavLink.removeAttribute('onclick');
newGdf.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
openInlineFavorites();
});
oldGdf.parentNode.replaceChild(newGdf, oldGdf);
}
purgeForeignPopups();
}
function attachRemoveHandler(btn, form, container) {
if (!btn || btn.__eh_bound) return;
btn.__eh_bound = true;
btn.addEventListener('click', async (e) => {
e.preventDefault();
btn.disabled = true;
btn.value = 'Removing...';
const submitBtn = container.querySelector('.eh-fav-submit-btn');
const cancelBtn = container.querySelector('.eh-fav-cancel-btn');
if (submitBtn) submitBtn.disabled = true;
if (cancelBtn) cancelBtn.disabled = true;
const m = location.pathname.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
const gid = m ? m[1] : (window.gid || '');
const token = m ? m[2] : (window.token || '');
const postUrl = `${location.origin}/gallerypopups.php?gid=${gid}&t=${token}&act=addfav`;
const formData = new FormData();
formData.append('favcat', 'favdel');
formData.append('favnote', '');
formData.append('apply', 'Apply Changes');
formData.append('update', '1');
try {
const res = await fetch(postUrl, {
method: 'POST',
body: formData,
credentials: 'include'
});
if (res.ok) {
cachedPopupData = {
note: '',
slots: (cachedPopupData ? cachedPopupData.slots : ''),
names: (cachedPopupData ? cachedPopupData.names : getSavedFavNames()),
popupItems: (cachedPopupData ? cachedPopupData.popupItems : []),
isFavorited: false,
favcat: null
};
updateGalleryFavoriteUI('favdel', 'Remove from Favorites');
closeInlineFavorites();
} else {
throw new Error('HTTP ' + res.status);
}
} catch (err) {
console.warn('ExFastReader: Failed to remove favorite', err);
alert('Failed to remove favorite. Please try again.');
btn.disabled = false;
btn.value = 'Remove from Favorites';
if (submitBtn) submitBtn.disabled = false;
if (cancelBtn) cancelBtn.disabled = false;
}
});
}
// Silently prefetch popup info (exact diamonds, note, slots, custom names) straight from the popup in background
async function silentPrefetchPopupInfo() {
if (cachedPopupData && typeof cachedPopupData.isFavorited === 'boolean') return cachedPopupData;
const m = location.pathname.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
const gid = m ? m[1] : (window.gid || '');
const token = m ? m[2] : (window.token || '');
if (!gid || !token) return null;
const popupUrl = `${location.origin}/gallerypopups.php?gid=${gid}&t=${token}&act=addfav`;
try {
const res = await fetch(popupUrl, { credentials: 'include' });
if (!res.ok) return null;
const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
let note = '';
const noteEl = doc.querySelector('textarea[name="favnote"]');
if (noteEl) note = noteEl.value || noteEl.textContent || '';
let slots = '';
const mSlots = html.match(/(\d+\s*\/\s*\d+\s+favorite note slots used[^\n<\[]*(?:<a[^>]*>\[\?\]<\/a>)?)/i);
if (mSlots) {
slots = mSlots[1];
} else {
const allElements = doc.querySelectorAll('p, div');
for (const p of allElements) {
if (p.children.length <= 1 && p.textContent.includes('favorite note slots used')) {
slots = p.innerHTML.trim();
break;
}
}
}
if (slots && !slots.includes('href=')) {
slots += ` <a href="https://ehwiki.org/wiki/Favorite" target="_blank">[?]</a>`;
}
const catDivs = doc.querySelectorAll('.nosel > div, div.nosel > div');
const names = getSavedFavNames();
const popupItems = [];
if (catDivs.length >= 10) {
catDivs.forEach((div, i) => {
if (i < 10) {
const txt = div.textContent.trim();
if (txt && !txt.toLowerCase().includes('remove')) {
names[i] = txt;
}
const diamondDiv = div.querySelector('div.i, div[style*="background"]');
const diamondStyle = diamondDiv ? diamondDiv.getAttribute('style') : `background-image:url('https://ehgt.org/g/fav.png'); background-position:0px ${-2 - 19 * i}px;`;
popupItems.push({ idx: i, name: txt || names[i], style: diamondStyle });
}
});
safeStorageSet(localStorage, 'eh_fav_cat_names', JSON.stringify(names));
}
const hasApply = Boolean(doc.querySelector('input[value*="Apply Changes"], input[value="Apply Changes"]'));
const hasRemove = Boolean(doc.querySelector('input[value*="Remove"], [onclick*="favdel"]'));
const checkedRadio = doc.querySelector('input[name="favcat"]:checked');
const checkedCat = checkedRadio ? parseInt(checkedRadio.value, 10) : null;
const isFavorited = hasApply || hasRemove || (checkedCat !== null && isGalleryFavorited());
const favcat = checkedCat !== null ? checkedCat : (detectCurrentCategory() ?? 0);
cachedPopupData = { note, slots, names, popupItems, isFavorited, favcat };
// If the menu is currently open, seamlessly sync the extracted fields
const container = document.getElementById('eh-inline-favorites-container');
if (container && container.style.display !== 'none') {
const textarea = container.querySelector('#eh-fav-note-textarea');
if (textarea && !textarea.value && note) {
textarea.value = note;
}
if (slots) {
const slotsEl = container.querySelector('.eh-fav-slots');
if (slotsEl) slotsEl.innerHTML = slots;
}
if (popupItems.length > 0) {
popupItems.forEach((item) => {
const label = container.querySelector(`.eh-fav-cat-label[data-cat-idx="${item.idx}"]`);
if (label && label.textContent !== item.name) {
label.textContent = item.name;
label.title = item.name;
}
const diamond = container.querySelector(`.eh-fav-cat-diamond[data-cat-idx="${item.idx}"]`);
if (diamond && item.style) {
diamond.setAttribute('style', item.style);
}
});
}
const submitBtn = container.querySelector('.eh-fav-submit-btn');
if (submitBtn) {
submitBtn.value = isFavorited ? 'Apply Changes' : 'Add to Favorites';
}
const targetRadio = container.querySelector(`input[name="favcat"][value="${favcat}"]`);
if (targetRadio) {
targetRadio.checked = true;
}
let removeBtn = container.querySelector('#eh-fav-remove-btn');
if (isFavorited) {
if (!removeBtn) {
removeBtn = document.createElement('input');
removeBtn.type = 'button';
removeBtn.className = 'eh-fav-remove-btn';
removeBtn.id = 'eh-fav-remove-btn';
removeBtn.value = 'Remove from Favorites';
const actionsEl = container.querySelector('.eh-fav-actions');
const cancelBtn = container.querySelector('.eh-fav-cancel-btn');
if (actionsEl && cancelBtn) {
actionsEl.insertBefore(removeBtn, cancelBtn);
}
const formEl = container.querySelector('#eh-fav-form');
attachRemoveHandler(removeBtn, formEl, container);
}
} else if (removeBtn) {
removeBtn.remove();
}
}
return cachedPopupData;
} catch {
return null;
}
}
function openInlineFavorites() {
const now = Date.now();
if (now - lastFavToggleTime < 300) return;
lastFavToggleTime = now;
purgeForeignPopups();
const gd4 = document.getElementById('gd4');
if (!gd4) return;
let container = document.getElementById('eh-inline-favorites-container');
if (container && container.style.display !== 'none') {
closeInlineFavorites();
return;
}
// Clean up any stray containers
const allContainers = document.querySelectorAll('#eh-inline-favorites-container');
if (allContainers.length > 1) {
for (let i = 1; i < allContainers.length; i++) allContainers[i].remove();
container = allContainers[0];
}
// Hide ALL other elements inside #gd4 (tag list, voting, and enter new tags boxes)
gd4.classList.add('eh-fav-active');
Array.from(gd4.children).forEach((child) => {
if (child !== container) {
if (child._eh_prev_display === undefined) child._eh_prev_display = child.style.display;
child.style.setProperty('display', 'none', 'important');
}
});
document.querySelectorAll('#tagmenu_new, #tagmenu_act, #tagmenu, #tagform').forEach((el) => {
if (el.closest('#gd4')) {
el.style.setProperty('display', 'none', 'important');
}
});
if (!container) {
container = document.createElement('div');
container.id = 'eh-inline-favorites-container';
gd4.appendChild(container);
}
container.style.display = 'block';
// INSTANT SYNCHRONOUS BUILD: 0ms delay, no loading spinner
renderBuiltInFavoriteForm(container);
// Silently sync note/slots/folder names in the background
silentPrefetchPopupInfo();
}
function closeInlineFavorites() {
const gd4 = document.getElementById('gd4');
if (gd4) {
gd4.classList.remove('eh-fav-active');
Array.from(gd4.children).forEach((child) => {
if (child.id !== 'eh-inline-favorites-container') {
child.style.display = child._eh_prev_display !== undefined ? child._eh_prev_display : '';
}
});
}
const container = document.getElementById('eh-inline-favorites-container');
if (container) {
container.style.display = 'none';
}
}
function updateGalleryFavoriteUI(catVal, submitVal) {
const favDiv = document.getElementById('fav');
const favLink = document.getElementById('favoritelink');
const gdf = document.getElementById('gdf');
if (catVal === 'favdel' || (submitVal && submitVal.toLowerCase().includes('remove'))) {
if (favDiv) {
favDiv.style.background = '';
favDiv.style.width = '';
favDiv.style.height = '';
favDiv.style.display = '';
favDiv.style.marginRight = '';
favDiv.removeAttribute('title');
favDiv.innerHTML = '';
}
if (favLink) {
favLink.innerHTML = savedFavLinkInitialHtml || '<img src="https://ehgt.org/g/mr.gif" /> Add to Favorites';
}
if (gdf) {
gdf.querySelectorAll('.i, img[src*="fav.png"], div[style*="fav.png"]').forEach((el) => {
if (el !== favDiv) el.remove();
});
}
cachedPopupData = {
note: '',
slots: (cachedPopupData ? cachedPopupData.slots : ''),
names: (cachedPopupData ? cachedPopupData.names : getSavedFavNames()),
popupItems: (cachedPopupData ? cachedPopupData.popupItems : []),
isFavorited: false,
favcat: null
};
} else if (catVal !== null && catVal !== undefined) {
const catIdx = parseInt(catVal, 10);
if (!isNaN(catIdx)) {
const names = getSavedFavNames();
const chosenName = names[catIdx] || ('Favorites ' + catIdx);
if (favDiv) {
favDiv.innerHTML = '';
favDiv.style.background = `transparent url('https://ehgt.org/g/fav.png') 0px ${-2 - 19 * catIdx}px no-repeat`;
favDiv.style.width = '19px';
favDiv.style.height = '19px';
favDiv.style.display = 'inline-block';
favDiv.style.marginRight = '4px';
favDiv.title = chosenName;
}
if (favLink) {
favLink.textContent = chosenName;
}
if (gdf) {
gdf.querySelectorAll('.i, img[src*="fav.png"], div[style*="fav.png"]').forEach((el) => {
if (el !== favDiv) el.remove();
});
}
cachedPopupData = {
note: (cachedPopupData ? cachedPopupData.note : ''),
slots: (cachedPopupData ? cachedPopupData.slots : ''),
names,
popupItems: (cachedPopupData ? cachedPopupData.popupItems : []),
isFavorited: true,
favcat: catIdx
};
}
}
}
function renderBuiltInFavoriteForm(container) {
const currentCat = detectCurrentCategory();
const isAlreadyFav = isGalleryFavorited();
const submitText = isAlreadyFav ? 'Apply Changes' : 'Add to Favorites';
const checkedCat = currentCat !== null ? currentCat : 0;
const favNames = (cachedPopupData && cachedPopupData.names) ? cachedPopupData.names : getSavedFavNames();
const noteVal = (cachedPopupData && cachedPopupData.note) ? cachedPopupData.note : '';
const slotsHtml = (cachedPopupData && cachedPopupData.slots) ? cachedPopupData.slots : `0 / 1000 favorite note slots used. <a href="https://ehwiki.org/wiki/Favorite" target="_blank">[?]</a>`;
const popupItems = cachedPopupData && cachedPopupData.popupItems ? cachedPopupData.popupItems : [];
let catsHtml = '';
for (let i = 0; i < 10; i++) {
const name = favNames[i] || `Favorites ${i}`;
const isChecked = (i === checkedCat);
const customStyle = popupItems[i]?.style || `background-image:url('https://ehgt.org/g/fav.png'); background-position:0px ${-2 - 19 * i}px;`;
catsHtml += `
<label class="eh-fav-cat-item">
<input type="radio" name="favcat" value="${i}" ${isChecked ? 'checked' : ''} />
<div class="eh-fav-cat-diamond i" data-cat-idx="${i}" style="${customStyle}"></div>
<span class="eh-fav-cat-label" data-cat-idx="${i}" title="${name}">${name}</span>
</label>
`;
}
const removeBtnHtml = isAlreadyFav
? `<input type="button" class="eh-fav-remove-btn" id="eh-fav-remove-btn" value="Remove from Favorites" />`
: '';
const headerText = 'Choose a favorite category to save this gallery under. You can also add a note to it if you wish.';
container.innerHTML = `
<form class="eh-fav-form" id="eh-fav-form" method="post">
<input type="hidden" name="update" value="1" />
<p class="eh-fav-header">${headerText}</p>
<div class="eh-fav-body">
<div class="eh-fav-cats">
${catsHtml}
</div>
<div class="eh-fav-note-pane">
<div class="eh-fav-note-title">Favorite Note (Max 200 Characters)</div>
<textarea name="favnote" id="eh-fav-note-textarea" maxlength="200" class="eh-fav-note-textarea" placeholder="">${noteVal}</textarea>
<div class="eh-fav-slots">${slotsHtml}</div>
<div class="eh-fav-actions">
<input type="submit" name="apply" class="eh-fav-submit-btn" value="${submitText}" />
${removeBtnHtml}
<input type="button" class="eh-fav-cancel-btn" value="Cancel" />
</div>
</div>
</div>
</form>
`;
const form = container.querySelector('#eh-fav-form');
const cancelBtn = container.querySelector('.eh-fav-cancel-btn');
const submitBtn = container.querySelector('.eh-fav-submit-btn');
const removeBtn = container.querySelector('#eh-fav-remove-btn');
if (cancelBtn) {
cancelBtn.addEventListener('click', (e) => {
e.preventDefault();
closeInlineFavorites();
});
}
if (removeBtn) {
attachRemoveHandler(removeBtn, form, container);
}
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const curSubmitVal = submitBtn ? submitBtn.value : 'Add to Favorites';
const selectedRadio = form.querySelector('input[name="favcat"]:checked');
const chosenCat = selectedRadio ? selectedRadio.value : '0';
const chosenNote = form.querySelector('#eh-fav-note-textarea')?.value || '';
const m = location.pathname.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
const gid = m ? m[1] : (window.gid || '');
const token = m ? m[2] : (window.token || '');
const postUrl = `${location.origin}/gallerypopups.php?gid=${gid}&t=${token}&act=addfav`;
const formData = new FormData();
formData.append('favcat', chosenCat);
formData.append('favnote', chosenNote);
formData.append('apply', curSubmitVal);
formData.append('update', '1');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.value = 'Saving...';
}
if (removeBtn) removeBtn.disabled = true;
if (cancelBtn) cancelBtn.disabled = true;
try {
const res = await fetch(postUrl, {
method: 'POST',
body: formData,
credentials: 'include'
});
if (res.ok) {
cachedPopupData = {
note: chosenNote,
slots: (cachedPopupData ? cachedPopupData.slots : ''),
names: (cachedPopupData ? cachedPopupData.names : getSavedFavNames()),
popupItems: (cachedPopupData ? cachedPopupData.popupItems : []),
isFavorited: true,
favcat: parseInt(chosenCat, 10)
};
updateGalleryFavoriteUI(chosenCat, curSubmitVal);
closeInlineFavorites();
} else {
throw new Error('HTTP ' + res.status);
}
} catch (err) {
console.warn('ExFastReader: Failed to save favorite', err);
alert('Failed to save favorite. Please try again.');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.value = curSubmitVal;
}
if (removeBtn) removeBtn.disabled = false;
if (cancelBtn) cancelBtn.disabled = false;
}
});
}
}
function setupInlineFavorites() {
const favLink = document.getElementById('favoritelink');
if (favLink && favLink.querySelector('img')) {
savedFavLinkInitialHtml = favLink.innerHTML;
}
sanitizeFavElements();
silentPrefetchPopupInfo();
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const container = document.getElementById('eh-inline-favorites-container');
if (container && container.style.display !== 'none') {
closeInlineFavorites();
}
}
});
}
function initGalleryPage() {
setupInlineFavorites();
const m = location.pathname.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
const curGid = m ? parseInt(m[1], 10) : 0;
if (m) {
const gUrl = location.origin + `/g/${m[1]}/${m[2]}/`;
safeStorageSet(sessionStorage, 'eh_gallery_' + m[1], gUrl);
safeStorageSet(localStorage, 'eh_gallery_' + m[1], gUrl);
cachedGalleryUrl = gUrl;
}
if (curGid) {
cachedGid = curGid;
loadGalleryDataFromSession(curGid);
}
const gpcEl = document.querySelector('.gpc, #gpc');
if (gpcEl) {
const mGpc = gpcEl.textContent.match(/Showing\s+(\d+)\s*-\s*(\d+)\s+of\s+([\d,]+)\s+images/i);
if (mGpc) {
const sNum = parseInt(mGpc[1], 10);
const eNum = parseInt(mGpc[2], 10);
const tot = parseInt(mGpc[3].replace(/,/g, ''), 10);
if (eNum < tot) {
const count = eNum - sNum + 1;
if ([20, 40, 100].includes(count)) {
thumbsPerPage = count;
safeStorageSet(localStorage, 'eh_thumbs_per_page', String(count));
}
}
if (tot > 0) cachedTotalPages = tot;
}
}
const thumbCount = document.querySelectorAll('#gdt .gdtm, #gdt .gdtl').length;
if ([20, 40, 100].includes(thumbCount)) {
thumbsPerPage = thumbCount;
safeStorageSet(localStorage, 'eh_thumbs_per_page', String(thumbCount));
}
// Immediately parse all thumbnails, dimensions, keys, and sprites directly from the gallery DOM
extractDimensionsFromGallery(document);
if (curGid) {
saveGalleryDataToSession(curGid);
}
if ('scrollRestoration' in history) {
try { history.scrollRestoration = 'manual'; } catch {}
}
const MAX_PREFETCH_LINKS = 12;
const prefetchLinks = [];
function safePrefetchUrl(url) {
if (!url) return;
try {
if (document.querySelector(`link[rel="prefetch"][href="${CSS.escape(url)}"]`)) return;
while (prefetchLinks.length >= MAX_PREFETCH_LINKS) {
const oldLink = prefetchLinks.shift();
if (oldLink && oldLink.parentNode) {
oldLink.remove();
}
}
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
document.head.appendChild(link);
prefetchLinks.push(link);
} catch {}
}
let isGalleryNavigating = false;
async function navigateGalleryPage(targetUrl, pushState = true) {
if (isGalleryNavigating) return;
isGalleryNavigating = true;
const gdt = document.getElementById('gdt');
const ptt = document.querySelector('table.ptt');
const ptb = document.querySelector('table.ptb');
const gpc = document.querySelector('.gpc, #gpc');
try {
const res = await fetch(targetUrl, { credentials: 'include' });
if (!res.ok) {
location.href = targetUrl;
return;
}
const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const newPtt = doc.querySelector('table.ptt');
const newGpc = doc.querySelector('.gpc, #gpc');
const newGdt = doc.getElementById('gdt');
const newPtb = doc.querySelector('table.ptb');
// 1. Update in natural top-to-bottom DOM order to eliminate reflow jumping
if (newPtt && ptt) {
ptt.innerHTML = newPtt.innerHTML;
}
if (newGpc && gpc) {
gpc.innerHTML = newGpc.innerHTML;
}
if (newGdt && gdt) {
gdt.innerHTML = newGdt.innerHTML;
}
if (newPtb && ptb) {
ptb.innerHTML = newPtb.innerHTML;
}
if (doc.title) {
document.title = doc.title;
}
if (pushState) {
window.history.pushState({ galleryUrl: targetUrl }, doc.title || document.title, targetUrl);
}
// 2. Parse and cache the newly loaded thumbnails and dimensions
extractDimensionsFromGallery(doc);
if (curGid) {
saveGalleryDataToSession(curGid);
}
// 3. Prefetch next page thumbnail if present
const nextP = document.querySelector('table.ptb td.ptds + td a') || document.querySelector('table.ptb td:last-child a');
if (nextP && nextP.href && nextP.textContent.trim() !== '<') {
safePrefetchUrl(nextP.href);
}
} catch (err) {
console.warn('ExFastReader: Gallery AJAX navigation failed, falling back to full navigation', err);
location.href = targetUrl;
} finally {
isGalleryNavigating = false;
}
}
window.__eh_navigateGalleryPage = navigateGalleryPage;
document.addEventListener('click', (e) => {
const cell = e.target.closest('table.ptt td, table.ptb td, table.ptt a, table.ptb a, .searchnav td, .searchnav a');
if (!cell) return;
const link = cell.tagName === 'A' ? cell : cell.querySelector('a');
if (!link) return;
const rawHref = link.getAttribute('href') || link.href;
if (!rawHref || rawHref === '#' || rawHref.startsWith('javascript:')) return;
let targetUrl;
try {
targetUrl = new URL(rawHref, location.href).href;
} catch {
targetUrl = link.href;
}
if (!targetUrl) return;
try {
const parsedTarget = new URL(targetUrl, location.origin);
if (parsedTarget.origin !== location.origin) return;
const mCur = location.pathname.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
const mTar = parsedTarget.pathname.match(/\/g\/(\d+)\/([0-9a-fA-F]{10})/i);
const isSameGallery = (mCur && mTar && mCur[1] === mTar[1]) ||
(parsedTarget.pathname.replace(/\/+$/, '') === location.pathname.replace(/\/+$/, ''));
if (!isSameGallery) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
navigateGalleryPage(targetUrl, true);
} catch (err) {
console.warn('ExFastReader: Gallery link check error', err);
}
}, true);
// Listen for reader navigation clicks to guarantee session storage synchronization
document.addEventListener('click', (e) => {
const a = e.target.closest('a[href*="/s/"]');
if (a && curGid) {
saveGalleryDataToSession(curGid);
}
}, { capture: true, passive: true });
// Prefetch reader page on hover for instant transition
document.addEventListener('pointerover', (e) => {
const a = e.target.closest('a[href*="/s/"]');
if (a && a.href && !a.__eh_prefetched) {
a.__eh_prefetched = true;
safePrefetchUrl(a.href);
}
}, { capture: true, passive: true });
const nextThumb = document.querySelector('table.ptb td.ptds + td a') || document.querySelector('table.ptb td:last-child a');
if (nextThumb && nextThumb.href && nextThumb.textContent.trim() !== '<') {
safePrefetchUrl(nextThumb.href);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initGalleryPage, { once: true });
} else {
initGalleryPage();
}
} else if (isIndexPage) {
initIndexInfiniteScroll();
}
}
})();