11import { cssEscape } from "../utils/css-escape" ;
2- import { fetchKeygenCatalog , fetchKeygenModule , type KeygenEntry } from "../sources/keygen" ;
3- import { fetchModlandCatalog , fetchModlandModule , type ModlandEntry } from "../sources/modland" ;
2+ import { getBrowserSource , type BrowserSongEntry , type BrowserSourceId } from "../sources" ;
43import { readStorage , writeStorage } from "../utils/storage" ;
54
6- export type BrowserSourceId = "modland" | "keygen" ;
7-
8- export interface BrowserSongEntry {
9- source : BrowserSourceId ;
10- path : string ;
11- fileName : string ;
12- tracker : string ;
13- artist : string ;
14- title : string ;
15- ext : string ;
16- sizeKb : number ;
17- searchText : string ;
18- rawEntry : ModlandEntry | KeygenEntry ;
19- }
5+ export type { BrowserSongEntry , BrowserSourceId } from "../sources" ;
206
217export interface TrackBrowserElements {
228 searchInput : HTMLInputElement ;
@@ -41,8 +27,6 @@ export interface TrackBrowserOptions {
4127export class TrackBrowser {
4228 private readonly elements : TrackBrowserElements ;
4329 private readonly options : TrackBrowserOptions ;
44- private readonly entriesBySource = new Map < BrowserSourceId , BrowserSongEntry [ ] > ( ) ;
45- private readonly catalogPromises = new Map < BrowserSourceId , Promise < void > > ( ) ;
4630 private filteredEntries : BrowserSongEntry [ ] = [ ] ;
4731 private sourceId : BrowserSourceId = "modland" ;
4832 private activePath = "" ;
@@ -84,8 +68,9 @@ export class TrackBrowser {
8468 async setSource ( sourceId : BrowserSourceId , autoLoad = true ) : Promise < void > {
8569 this . sourceId = sourceId ;
8670 this . page = 0 ;
71+ const source = this . getSource ( ) ;
8772
88- if ( this . entriesBySource . has ( sourceId ) ) {
73+ if ( source . hasLoadedEntries ( ) ) {
8974 this . applyFilter ( ) ;
9075 return ;
9176 }
@@ -137,41 +122,29 @@ export class TrackBrowser {
137122 }
138123
139124 async initCatalog ( ) : Promise < void > {
140- if ( this . entriesBySource . has ( this . sourceId ) ) {
125+ const source = this . getSource ( ) ;
126+ if ( source . hasLoadedEntries ( ) ) {
141127 this . applyFilter ( ) ;
142128 return ;
143129 }
144130
145- const existingPromise = this . catalogPromises . get ( this . sourceId ) ;
146- if ( existingPromise ) {
147- return existingPromise ;
148- }
149-
150131 this . updatePagination ( 0 , 0 , "Loading..." ) ;
151132
152133 const sourceId = this . sourceId ;
153- const catalogPromise = ( async ( ) => {
154- try {
155- const entries = await this . loadEntriesForSource ( sourceId ) ;
156- this . entriesBySource . set ( sourceId , entries ) ;
157-
158- if ( this . sourceId === sourceId ) {
159- this . applyFilter ( ) ;
160- this . options . onStatusChange ( "IDLE" ) ;
161- }
162- } catch ( error ) {
163- console . error ( `Failed to load ${ sourceId } catalog:` , error ) ;
164- if ( this . sourceId === sourceId ) {
165- this . updatePagination ( 0 , 0 , "Unavailable" ) ;
166- this . options . onStatusChange ( `${ sourceId . toUpperCase ( ) } CATALOG FAILED` ) ;
167- }
168- } finally {
169- this . catalogPromises . delete ( sourceId ) ;
170- }
171- } ) ( ) ;
134+ try {
135+ await this . getSource ( sourceId ) . getEntries ( ) ;
172136
173- this . catalogPromises . set ( sourceId , catalogPromise ) ;
174- return catalogPromise ;
137+ if ( this . sourceId === sourceId ) {
138+ this . applyFilter ( ) ;
139+ this . options . onStatusChange ( "IDLE" ) ;
140+ }
141+ } catch ( error ) {
142+ console . error ( `Failed to load ${ sourceId } catalog:` , error ) ;
143+ if ( this . sourceId === sourceId ) {
144+ this . updatePagination ( 0 , 0 , "Unavailable" ) ;
145+ this . options . onStatusChange ( `${ sourceId . toUpperCase ( ) } CATALOG FAILED` ) ;
146+ }
147+ }
175148 }
176149
177150 async loadSongByPath ( pathOrFileName : string , autoplay = true ) : Promise < boolean > {
@@ -212,6 +185,7 @@ export class TrackBrowser {
212185 if ( next ) {
213186 next . classList . add ( "active" ) ;
214187 this . selectedSongItem = next ;
188+ this . scrollSelectedSongIntoView ( ) ;
215189 }
216190 }
217191
@@ -257,13 +231,14 @@ export class TrackBrowser {
257231 return ;
258232 }
259233
260- this . filteredEntries = this . filterEntries ( this . getCurrentEntries ( ) , this . elements . searchInput . value ) ;
234+ this . filteredEntries = this . getSource ( ) . filterLoadedEntries ( this . elements . searchInput . value ) ;
261235 this . page = 0 ;
262236 this . renderSongList ( ) ;
263237 }
264238
265239 private renderSongList ( ) : void {
266240 this . elements . songList . replaceChildren ( ) ;
241+ this . selectedSongItem = null ;
267242
268243 const totalPages = this . getTotalPages ( ) ;
269244 if ( totalPages === 0 ) {
@@ -346,7 +321,7 @@ export class TrackBrowser {
346321 } ) ;
347322 await this . options . onLoadModule ( entry , module . buffer , module . fileName , autoplay ) ;
348323 } catch ( error ) {
349- console . error ( " Failed to load Modland module:" , error ) ;
324+ console . error ( ` Failed to load ${ entry . source } module:` , error ) ;
350325 this . options . onStatusChange ( "MODULE LOAD FAILED" ) ;
351326 } finally {
352327 this . clearLoadingProgress ( ) ;
@@ -390,6 +365,13 @@ export class TrackBrowser {
390365 return this . filteredEntries . findIndex ( ( entry ) => this . matchesEntry ( entry , this . activePath ) ) ;
391366 }
392367
368+ private scrollSelectedSongIntoView ( ) : void {
369+ this . selectedSongItem ?. scrollIntoView ( {
370+ block : "nearest" ,
371+ inline : "nearest" ,
372+ } ) ;
373+ }
374+
393375 private async loadRelativeSong ( offset : - 1 | 1 ) : Promise < void > {
394376 if ( this . loading ) {
395377 return ;
@@ -416,68 +398,17 @@ export class TrackBrowser {
416398 }
417399
418400 private getCurrentEntries ( ) : BrowserSongEntry [ ] {
419- return this . entriesBySource . get ( this . sourceId ) ?? [ ] ;
420- }
421-
422- private filterEntries ( entries : BrowserSongEntry [ ] , query : string ) : BrowserSongEntry [ ] {
423- const normalized = query . trim ( ) . toLowerCase ( ) ;
424- if ( ! normalized ) {
425- return entries ;
426- }
427-
428- const terms = normalized . split ( / \s + / ) . filter ( Boolean ) ;
429- return entries . filter ( ( entry ) => terms . every ( ( term ) => entry . searchText . includes ( term ) ) ) ;
430- }
431-
432- private async loadEntriesForSource ( sourceId : BrowserSourceId ) : Promise < BrowserSongEntry [ ] > {
433- if ( sourceId === "keygen" ) {
434- const entries = await fetchKeygenCatalog ( ) ;
435- return entries . map ( ( entry ) => this . mapKeygenEntry ( entry ) ) ;
436- }
437-
438- const entries = await fetchModlandCatalog ( ) ;
439- return entries . map ( ( entry ) => this . mapModlandEntry ( entry ) ) ;
401+ return this . getSource ( ) . getLoadedEntries ( ) ;
440402 }
441403
442404 private async fetchModule (
443405 entry : BrowserSongEntry ,
444406 onProgress : ( progressPercent : number ) => void ,
445407 ) : Promise < { buffer : ArrayBuffer ; fileName : string } > {
446- if ( entry . source === "keygen" ) {
447- return fetchKeygenModule ( entry . rawEntry as KeygenEntry , onProgress ) ;
448- }
449-
450- return fetchModlandModule ( entry . rawEntry as ModlandEntry , onProgress ) ;
451- }
452-
453- private mapModlandEntry ( entry : ModlandEntry ) : BrowserSongEntry {
454- return {
455- source : "modland" ,
456- path : entry . path ,
457- fileName : entry . archiveEntryName ,
458- tracker : entry . tracker ,
459- artist : entry . artist || "Unknown" ,
460- title : entry . title || entry . archiveEntryName ,
461- ext : entry . ext ,
462- sizeKb : entry . sizeKb ,
463- searchText : `${ entry . title } ${ entry . artist } ${ entry . tracker } ${ entry . ext } ` . toLowerCase ( ) ,
464- rawEntry : entry ,
465- } ;
408+ return this . getSource ( entry . source ) . fetchModule ( entry , onProgress ) ;
466409 }
467410
468- private mapKeygenEntry ( entry : KeygenEntry ) : BrowserSongEntry {
469- return {
470- source : "keygen" ,
471- path : entry . path ,
472- fileName : entry . fileName ,
473- tracker : entry . tracker ,
474- artist : entry . artist || "Unknown" ,
475- title : entry . title || entry . fileName ,
476- ext : entry . ext ,
477- sizeKb : entry . sizeKb ,
478- searchText :
479- `${ entry . title } ${ entry . trackTitle } ${ entry . artist } ${ entry . tracker } ${ entry . ext } ${ entry . fileName } ` . toLowerCase ( ) ,
480- rawEntry : entry ,
481- } ;
411+ private getSource ( sourceId = this . sourceId ) {
412+ return getBrowserSource ( sourceId ) ;
482413 }
483414}
0 commit comments