44 * Handles loading and displaying content page by page with spread mode support
55 */
66
7- /* global URL, Image */
7+ /* global URL, Image, IntersectionObserver */
88
99import { APIClient , APIHelper } from '../core/api.js' ;
1010import { mediaWorker , Priority } from './media-worker-manager.js' ;
@@ -71,6 +71,7 @@ export class PageReader {
7171 this . prefetchCache = new Map ( ) ; // Cache for prefetched images
7272 this . workerInitialized = false ; // Media worker status
7373 this . _displayBlobUrls = [ ] ; // Blob URLs for currently displayed pages (for cleanup)
74+ this . _thumbnailObserver = null ; // IntersectionObserver for lazy thumbnail loading
7475
7576 // Initialize managers
7677 this . fullscreenManager = new FullscreenManager ( {
@@ -207,7 +208,6 @@ export class PageReader {
207208 return ;
208209 }
209210
210- // Use a data attribute to store the reader reference name for onclick
211211 const readerName = this . contentType === 'comic' ? 'comicReader' : 'pdfReader' ;
212212
213213 pageList . innerHTML = this . metadata . pages
@@ -219,16 +219,52 @@ export class PageReader {
219219 onclick="${ readerName } .loadPage(${ index } )"
220220 >
221221 <img
222- src="${ this . getEndpoint ( `page/${ index } /thumbnail` ) } "
222+ data- src="${ this . getEndpoint ( `page/${ index } /thumbnail` ) } "
223223 alt="Page ${ index + 1 } "
224224 class="page-thumbnail"
225- loading="lazy"
226225 />
227226 <span class="page-number">Page ${ index + 1 } </span>
228227 </div>
229228 `
230229 )
231230 . join ( '' ) ;
231+
232+ this . _setupThumbnailObserver ( pageList ) ;
233+ }
234+
235+ /**
236+ * Set up IntersectionObserver to lazy-load sidebar thumbnails with auth.
237+ * @param {HTMLElement } pageList - The sidebar page list container
238+ */
239+ _setupThumbnailObserver ( pageList ) {
240+ if ( this . _thumbnailObserver ) {
241+ this . _thumbnailObserver . disconnect ( ) ;
242+ }
243+
244+ this . _thumbnailObserver = new IntersectionObserver (
245+ ( entries ) => {
246+ entries . forEach ( ( entry ) => {
247+ if ( ! entry . isIntersecting ) return ;
248+ const img = entry . target ;
249+ const src = img . dataset . src ;
250+ if ( ! src || img . src . startsWith ( 'blob:' ) ) return ;
251+ this . _thumbnailObserver . unobserve ( img ) ;
252+ APIClient . authenticatedFetch ( src )
253+ . then ( ( response ) => response . blob ( ) )
254+ . then ( ( blob ) => {
255+ img . src = URL . createObjectURL ( blob ) ;
256+ } )
257+ . catch ( ( ) => {
258+ // leave placeholder on error
259+ } ) ;
260+ } ) ;
261+ } ,
262+ { root : pageList , rootMargin : '200px' }
263+ ) ;
264+
265+ pageList . querySelectorAll ( 'img[data-src]' ) . forEach ( ( img ) => {
266+ this . _thumbnailObserver . observe ( img ) ;
267+ } ) ;
232268 }
233269
234270 /**
@@ -768,6 +804,11 @@ export class PageReader {
768804 }
769805 this . prefetchCache . clear ( ) ;
770806 this . _revokePreviousDisplayBlobs ( ) ;
807+ if ( this . _thumbnailObserver ) {
808+ this . _thumbnailObserver . disconnect ( ) ;
809+ this . _thumbnailObserver = null ;
810+ }
811+ this . _revokePreviousDisplayBlobs ( ) ;
771812 }
772813
773814 /**
0 commit comments