11/**
22 * Image cache for preloading and decoding manga pages
33 * Maintains a windowed cache: previous 2 + current + next 3 pages
4+ *
5+ * Public API is index-based for clean caller usage.
6+ * Uses fuzzy matching to align files with pages when paths don't match exactly.
47 */
58
9+ import type { Page } from '$lib/types' ;
10+ import { normalizeFilename } from '$lib/util/misc' ;
11+
612export interface CachedImage {
713 image : HTMLImageElement ; // Image element holds decoded bitmap and blob URL (in img.src)
814 decoded : boolean ;
915 loading : Promise < void > | null ;
1016}
1117
18+ /**
19+ * Extract just the filename from a path (handles both / and \ separators)
20+ */
21+ function getBasename ( path : string ) : string {
22+ return path . split ( / [ / \\ ] / ) . pop ( ) || path ;
23+ }
24+
25+ /**
26+ * Natural sort comparator for filenames
27+ */
28+ function naturalSort ( a : string , b : string ) : number {
29+ return a . localeCompare ( b , undefined , { numeric : true , sensitivity : 'base' } ) ;
30+ }
31+
32+ /**
33+ * Match files to pages using fuzzy matching strategies
34+ * Returns an indexed array of Files aligned with page order
35+ *
36+ * Strategy order:
37+ * 1. Exact path match - all page.img_path values match file keys exactly
38+ * 2. Basename match - match just the filename portion without directories
39+ * 3. Page order fallback - sort files naturally and align by index
40+ */
41+ function matchFilesToPages ( files : Record < string , File > , pages : Page [ ] ) : File [ ] {
42+ const fileKeys = Object . keys ( files ) ;
43+ const result : File [ ] = new Array ( pages . length ) ;
44+
45+ // Build a normalized path -> original key mapping for lookups
46+ const normalizedToKey = new Map < string , string > ( ) ;
47+ for ( const key of fileKeys ) {
48+ normalizedToKey . set ( normalizeFilename ( key ) , key ) ;
49+ }
50+
51+ // Strategy 1: Try exact path matching (with normalization)
52+ let allExactMatches = true ;
53+ for ( let i = 0 ; i < pages . length ; i ++ ) {
54+ const imgPath = pages [ i ] . img_path ;
55+ const normalizedImgPath = normalizeFilename ( imgPath ) ;
56+
57+ // Try direct match first, then normalized match
58+ if ( files [ imgPath ] ) {
59+ result [ i ] = files [ imgPath ] ;
60+ } else if ( normalizedToKey . has ( normalizedImgPath ) ) {
61+ result [ i ] = files [ normalizedToKey . get ( normalizedImgPath ) ! ] ;
62+ } else {
63+ allExactMatches = false ;
64+ break ;
65+ }
66+ }
67+
68+ if ( allExactMatches ) {
69+ return result ;
70+ }
71+
72+ // Strategy 2: Try basename matching (with normalization)
73+ // Build a map of normalized basename -> file for all files
74+ const basenameToFile = new Map < string , File > ( ) ;
75+ const basenameConflicts = new Set < string > ( ) ;
76+
77+ for ( const key of fileKeys ) {
78+ const basename = normalizeFilename ( getBasename ( key ) ) ;
79+ if ( basenameToFile . has ( basename ) ) {
80+ basenameConflicts . add ( basename ) ;
81+ } else {
82+ basenameToFile . set ( basename , files [ key ] ) ;
83+ }
84+ }
85+
86+ let allBasenameMatches = true ;
87+ for ( let i = 0 ; i < pages . length ; i ++ ) {
88+ const imgPath = pages [ i ] . img_path ;
89+ const basename = normalizeFilename ( getBasename ( imgPath ) ) ;
90+
91+ if ( basenameConflicts . has ( basename ) ) {
92+ allBasenameMatches = false ;
93+ break ;
94+ }
95+
96+ const file = basenameToFile . get ( basename ) ;
97+ if ( file ) {
98+ result [ i ] = file ;
99+ } else {
100+ allBasenameMatches = false ;
101+ break ;
102+ }
103+ }
104+
105+ if ( allBasenameMatches ) {
106+ return result ;
107+ }
108+
109+ // Strategy 3: Fall back to page order (sort files naturally)
110+ const sortedKeys = fileKeys . sort ( naturalSort ) ;
111+ for ( let i = 0 ; i < pages . length && i < sortedKeys . length ; i ++ ) {
112+ result [ i ] = files [ sortedKeys [ i ] ] ;
113+ }
114+
115+ return result ;
116+ }
117+
12118export class ImageCache {
13- private cache = new Map < number , CachedImage > ( ) ;
14- private files : File [ ] = [ ] ;
119+ private cache = new Map < number , CachedImage > ( ) ; // Keyed by page index
120+ private files : File [ ] = [ ] ; // Indexed array aligned with pages
121+ private pages : Page [ ] = [ ] ;
15122 private currentIndex = 0 ;
16123 private windowSize = { prev : 2 , next : 3 } ;
17124
18125 /**
19126 * Initialize or update the cache with new files and current page
20127 * Returns immediately - all preloading happens in the background
21128 */
22- updateCache ( files : File [ ] , currentIndex : number ) : void {
23- const filesChanged = this . files !== files ;
129+ updateCache ( files : Record < string , File > , pages : Page [ ] , currentIndex : number ) : void {
130+ // Detect if we have new files by checking reference and length
131+ const fileCount = Object . keys ( files ) . length ;
132+ const filesChanged = this . files . length !== fileCount || this . pages !== pages ;
24133
25- // Clear old cache if files changed
134+ // Clear old cache and build indexed files array if files changed
26135 if ( filesChanged ) {
27136 this . cleanup ( ) ;
28- this . files = files ;
137+ this . files = matchFilesToPages ( files , pages ) ;
138+ this . pages = pages ;
29139 }
30140
31141 this . currentIndex = currentIndex ;
32142
33143 // Calculate window range
34144 const startIndex = Math . max ( 0 , currentIndex - this . windowSize . prev ) ;
35- const endIndex = Math . min ( files . length - 1 , currentIndex + this . windowSize . next ) ;
145+ const endIndex = Math . min ( pages . length - 1 , currentIndex + this . windowSize . next ) ;
146+
147+ // Get indices in the window
148+ const windowIndices = new Set < number > ( ) ;
149+ for ( let i = startIndex ; i <= endIndex ; i ++ ) {
150+ windowIndices . add ( i ) ;
151+ }
36152
37153 // Remove items outside the window
38- for ( const [ index , cached ] of this . cache . entries ( ) ) {
39- if ( index < startIndex || index > endIndex ) {
154+ for ( const [ index ] of this . cache . entries ( ) ) {
155+ if ( ! windowIndices . has ( index ) ) {
40156 this . removeFromCache ( index ) ;
41157 }
42158 }
43159
44160 // Preload all items in the window (non-blocking)
45161 for ( let i = startIndex ; i <= endIndex ; i ++ ) {
46162 this . preloadImage ( i ) . catch ( ( err ) => {
47- console . error ( `Failed to preload image ${ i } :` , err ) ;
163+ console . error ( `Failed to preload image at index ${ i } :` , err ) ;
48164 } ) ;
49165 }
50166 }
51167
168+ /**
169+ * Get the File for a page index (for MangaPage fallback rendering)
170+ */
171+ getFile ( index : number ) : File | undefined {
172+ return this . files [ index ] ;
173+ }
174+
52175 /**
53176 * Get a cached image URL synchronously if it's ready, null otherwise
54177 */
55178 getImageSync ( index : number ) : string | null {
56- if ( index < 0 || index >= this . files . length ) {
57- return null ;
58- }
59-
60179 const cached = this . cache . get ( index ) ;
61180 if ( cached && cached . decoded ) {
62181 return cached . image . src ;
63182 }
64-
65183 return null ;
66184 }
67185
68186 /**
69187 * Get a cached image URL, waiting for it to be ready if necessary
70188 */
71189 async getImage ( index : number ) : Promise < string | null > {
72- if ( index < 0 || index >= this . files . length ) {
73- return null ;
74- }
75-
76190 const cached = this . cache . get ( index ) ;
77191 if ( cached ) {
78192 // Wait for image to be decoded if it's still loading
@@ -89,7 +203,7 @@ export class ImageCache {
89203 }
90204
91205 /**
92- * Preload and decode an image at the given index
206+ * Preload and decode an image by its page index
93207 */
94208 private async preloadImage ( index : number ) : Promise < void > {
95209 // Already cached
@@ -187,11 +301,11 @@ export class ImageCache {
187301 return {
188302 size : this . cache . size ,
189303 currentIndex : this . currentIndex ,
190- cached : Array . from ( this . cache . keys ( ) ) . sort ( ( a , b ) => a - b ) ,
304+ fileCount : this . files . length ,
305+ cached : Array . from ( this . cache . keys ( ) ) ,
191306 decoded : Array . from ( this . cache . entries ( ) )
192307 . filter ( ( [ _ , v ] ) => v . decoded )
193308 . map ( ( [ k ] ) => k )
194- . sort ( ( a , b ) => a - b )
195309 } ;
196310 }
197311}
0 commit comments