import { HttpClient } from '@angular/common/http' ; import { Injectable } from '@angular/core' ; import { File , FileEntry } from '@ionic-native/file' ; import { Platform } from 'ionic-angular' ; import { fromEvent } from 'rxjs/observable/fromEvent' ; import { first } from 'rxjs/operators' ; import { ImageLoaderConfig } from './image-loader-config' ; interface IndexItem { name : string ; modificationTime : Date ; size : number ; } interface QueueItem { imageUrl : string ; resolve : Function ; reject : Function ; } declare const Ionic ; @ Injectable ( ) export class ImageLoader { type { boolean } private isCacheReady = false ; type { boolean } private isInit = false ; type { number } private concurrency = 5 ; type { Array } private queue : QueueItem [ ] = [ ] ; private processing = 0 ; /**
     * Fast accessible Object for currently processing items
     */ private currentlyProcessing : { [ index ] : Promise < any > ; } = { } ; private cacheIndex : IndexItem [ ] = [ ] ; private currentCacheSize = 0 ; private indexed = false ; constructor ( private config , private file , private http , private platform ) { if ( ! platform . is ( 'cordova' ) ) { this . isInit = true ; this . throwWarning ( 'You are running on a browser or using livereload, IonicImageLoader will not function, falling back to browser loading.' ) ; } else { fromEvent ( document , 'deviceready' ) . pipe ( first ( ) ) . subscribe ( res => { if ( this . nativeAvailable ) { this . initCache ( ) ; } else { this . isInit = true ; this . throwWarning ( 'You are running on a browser or using livereload, IonicImageLoader will not function, falling back to browser loading.' ) ; } } ) ; } } get nativeAvailable ( ) : boolean { return File . installed ( ) ; } private get isCacheSpaceExceeded ( ) : boolean { return ( this . config . maxCacheSize > - 1 && this . currentCacheSize > this . config . maxCacheSize ) ; } private get isWKWebView ( ) : boolean { return ( this . platform . is ( 'ios' ) && ( < any > window ) . webkit && ( < any > window ) . webkit . messageHandlers ) ; } private get isIonicWKWebView ( ) : boolean { return ( ( this . isWKWebView || this . platform . is ( 'android' ) ) && ( location . host === 'localhost:8080' || ( < any > window ) . LiveReload ) ) ; } private get isDevServer ( ) : boolean { return window [ 'IonicDevServer' ] !== undefined ; } returns { boolean } private get canProcess ( ) : boolean { return this . queue . length > 0 && this . processing < this . concurrency ; } param { string } imageUrl returns { Promise < string > } preload ( imageUrl ) { return this . getImagePath ( imageUrl ) ; } getFileCacheDirectory ( ) { if ( this . config . cacheDirectoryType == 'data' ) { return this . file . dataDirectory ; } return this . file . cacheDirectory ; } param { string } imageUrl clearImageCache ( imageUrl ) { if ( ! this . platform . is ( 'cordova' ) ) { return ; } const clear = ( ) => { if ( ! this . isInit ) { setTimeout ( clear . bind ( this ) , 500 ) ; return ; } const fileName = this . createFileName ( imageUrl ) ; const route = this . getFileCacheDirectory ( ) + this . config . cacheDirectoryName ; this . isInit = false ; this . file . removeFile ( route , fileName ) . then ( ( ) => { if ( this . isWKWebView && ! this . isIonicWKWebView ) { this . file . removeFile ( this . file . tempDirectory + this . config . cacheDirectoryName , fileName ) . then ( ( ) => { this . initCache ( true ) ; } ) . catch ( err => { } ) ; } else { this . initCache ( true ) ; } } ) . catch ( this . throwError . bind ( this ) ) ; } ; clear ( ) ; } /**
     * Clears the cache
     */ clearCache ( ) { if ( ! this . platform . is ( 'cordova' ) ) { return ; } const clear = ( ) => { if ( ! this . isInit ) { setTimeout ( clear . bind ( this ) , 500 ) ; return ; } this . isInit = false ; this . file . removeRecursively ( this . getFileCacheDirectory ( ) , this . config . cacheDirectoryName ) . then ( ( ) => { if ( this . isWKWebView && ! this . isIonicWKWebView ) { this . file . removeRecursively ( this . file . tempDirectory , this . config . cacheDirectoryName ) . catch ( error => { } ) . then ( ( ) => { this . initCache ( true ) ; } ) ; } else { this . initCache ( true ) ; } } ) . catch ( this . throwError . bind ( this ) ) ; } ; clear ( ) ; } param { string } imageUrl returns { Promise < string > } getImagePath ( imageUrl ) { if ( typeof imageUrl !== 'string' || imageUrl . length <= 0 ) { return Promise . reject ( 'The image url provided was empty or invalid.' ) ; } return new Promise < string > ( ( resolve , reject ) => { const getImage = ( ) => { if ( this . isImageUrlRelative ( imageUrl ) ) { resolve ( imageUrl ) ; } else { this . getCachedImagePath ( imageUrl ) . then ( resolve ) . catch ( ( ) => { this . addItemToQueue ( imageUrl , resolve , reject ) ; } ) ; } } ; const check = ( ) => { if ( this . isInit ) { if ( this . isCacheReady ) { getImage ( ) ; } else { this . throwWarning ( 'The cache system is not running. Images will be loaded by your browser instead.' ) ; resolve ( imageUrl ) ; } } else { setTimeout ( ( ) => check ( ) , 250 ) ; } } ; check ( ) ; } ) ; } param { string } imageUrl private isImageUrlRelative ( imageUrl ) { return ! /^(https?|file):\/\/\/?/i . test ( imageUrl ) ; } param { string } imageUrl param resolve param reject private addItemToQueue ( imageUrl , resolve , reject ) { this . queue . push ( { imageUrl , resolve , reject , } ) ; this . processQueue ( ) ; } /**
     * Processes one item from the queue
     */ private processQueue ( ) { if ( ! this . canProcess ) { return ; } this . processing ++ ; const currentItem = this . queue . splice ( 0 , 1 ) [ 0 ] ; const done = ( ) => { this . processing -- ; this . processQueue ( ) ; if ( this . currentlyProcessing [ currentItem . imageUrl ] !== undefined && ! this . currentlyInQueue ( currentItem . imageUrl ) ) { delete this . currentlyProcessing [ currentItem . imageUrl ] ; } } ; const error = ( e ) => { currentItem . reject ( ) ; this . throwError ( e ) ; done ( ) ; } ; if ( this . currentlyProcessing [ currentItem . imageUrl ] === undefined ) { this . currentlyProcessing [ currentItem . imageUrl ] = new Promise ( ( resolve , reject ) => { if ( this . canProcess ) { this . processQueue ( ) ; } const localDir = this . getFileCacheDirectory ( ) + this . config . cacheDirectoryName + '/' ; const fileName = this . createFileName ( currentItem . imageUrl ) ; this . http . get ( currentItem . imageUrl , { responseType : 'blob' , headers : this . config . httpHeaders } ) . subscribe ( ( data ) => { this . file . writeFile ( localDir , fileName , data , { replace : true } ) . then ( ( file ) => { if ( this . isCacheSpaceExceeded ) { this . maintainCacheSize ( ) ; } this . addFileToIndex ( file ) . then ( ( ) => { this . getCachedImagePath ( currentItem . imageUrl ) . then ( ( localUrl ) => { currentItem . resolve ( localUrl ) ; resolve ( ) ; done ( ) ; this . maintainCacheSize ( ) ; } ) ; } ) ; } ) . catch ( ( e ) => { error ( e ) ; reject ( e ) ; } ) ; } , ( e ) => { error ( e ) ; reject ( e ) ; } ) ; } ) . catch ( ( e ) => this . throwError ( e ) ) ; } else { this . currentlyProcessing [ currentItem . imageUrl ] . then ( ( ) => { this . getCachedImagePath ( currentItem . imageUrl ) . then ( localUrl => { currentItem . resolve ( localUrl ) ; } ) ; done ( ) ; } , ( e ) => { error ( e ) ; } ) ; } } param imageUrl { string } returns { boolean } private currentlyInQueue ( imageUrl ) { return this . queue . some ( item => item . imageUrl === imageUrl ) ; } param boolean private initCache ( replace ? ) { this . concurrency = this . config . concurrency ; this . createCacheDirectory ( replace ) . catch ( e => { this . throwError ( e ) ; this . isInit = true ; } ) . then ( ( ) => this . indexCache ( ) ) . then ( ( ) => { this . isCacheReady = true ; this . isInit = true ; } ) ; } param { FileEntry } file returns { Promise < any > } private addFileToIndex ( file ) { return new Promise < any > ( ( resolve , reject ) => file . getMetadata ( resolve , reject ) ) . then ( metadata => { if ( this . config . maxCacheAge > - 1 && Date . now ( ) - metadata . modificationTime . getTime ( ) > this . config . maxCacheAge ) { return this . removeFile ( file . name ) ; } else { this . currentCacheSize += metadata . size ; this . cacheIndex . push ( { name : file . name , modificationTime : metadata . modificationTime , size : metadata . size , } ) ; return Promise . resolve ( ) ; } } ) ; } returns { Promise < void > } private indexCache ( ) { this . cacheIndex = [ ] ; return this . file . listDir ( this . getFileCacheDirectory ( ) , this . config . cacheDirectoryName ) . then ( files => Promise . all ( files . map ( this . addFileToIndex . bind ( this ) ) ) ) . then ( ( ) => { this . cacheIndex = this . cacheIndex . sort ( ( a , b ) : number => ( a > b ? - 1 : a < b ? 1 : 0 ) ) ; this . indexed = true ; return Promise . resolve ( ) ; } ) . catch ( e => { this . throwError ( e ) ; return Promise . resolve ( ) ; } ) ; } /**
     * This method runs every time a new file is added.
     * It checks the cache size and ensures that it doesn't exceed the maximum cache size set in the config.
     * If the limit is reached, it will delete old images to create free space.
     */ private maintainCacheSize ( ) { if ( this . config . maxCacheSize > - 1 && this . indexed ) { const maintain = ( ) => { if ( this . currentCacheSize > this . config . maxCacheSize ) { const next = ( ) => { this . currentCacheSize -= file . size ; maintain ( ) ; } ; const file = this . cacheIndex . splice ( 0 , 1 ) [ 0 ] ; if ( typeof file === 'undefined' ) { return maintain ( ) ; } this . removeFile ( file . name ) . then ( ( ) => next ( ) ) . catch ( ( ) => next ( ) ) ; } } ; maintain ( ) ; } } param { string } file returns { Promise < any > } private removeFile ( file ) { return this . file . removeFile ( this . getFileCacheDirectory ( ) + this . config . cacheDirectoryName , file ) . then ( ( ) => { if ( this . isWKWebView && ! this . isIonicWKWebView ) { return this . file . removeFile ( this . file . tempDirectory + this . config . cacheDirectoryName , file ) . catch ( ( ) => { } ) ; } } ) ; } param { string } url returns { Promise < string > } private getCachedImagePath ( url ) { return new Promise < string > ( ( resolve , reject ) => { if ( ! this . isCacheReady ) { return reject ( ) ; } if ( this . isDevServer ) { return resolve ( url ) ; } const fileName = this . createFileName ( url ) ; const dirPath = this . getFileCacheDirectory ( ) + this . config . cacheDirectoryName , tempDirPath = this . file . tempDirectory + this . config . cacheDirectoryName ; this . file . resolveLocalFilesystemUrl ( dirPath + '/' + fileName ) . then ( ( fileEntry ) => { if ( this . config . imageReturnType === 'base64' ) { this . file . readAsDataURL ( dirPath , fileName ) . then ( ( base64 ) => { base64 = base64 . replace ( 'data:null' , 'data:*/*' ) ; resolve ( base64 ) ; } ) . catch ( reject ) ; } else if ( this . config . imageReturnType === 'uri' ) { if ( this . isIonicWKWebView ) { resolve ( Ionic . WebView . convertFileSrc ( fileEntry . nativeURL ) ) ; } else if ( this . isWKWebView ) { this . file . resolveLocalFilesystemUrl ( tempDirPath + '/' + fileName ) . then ( ( tempFileEntry ) => { resolve ( Ionic . WebView . convertFileSrc ( tempFileEntry . nativeURL ) ) ; } ) . catch ( ( ) => { this . file . copyFile ( dirPath , fileName , tempDirPath , fileName ) . then ( ( tempFileEntry ) => { resolve ( Ionic . WebView . convertFileSrc ( tempFileEntry . nativeURL ) ) ; } ) . catch ( reject ) ; } ) ; } else { resolve ( Ionic . WebView . convertFileSrc ( fileEntry . nativeURL ) ) ; } } } ) . catch ( reject ) ; } ) ; } param { any [ ] } args private throwError ( ... args ) { if ( this . config . debugMode ) { args . unshift ( 'ImageLoader Error: ' ) ; console . error . apply ( console , args ) ; } } param { any [ ] } args private throwWarning ( ... args ) { if ( this . config . debugMode ) { args . unshift ( 'ImageLoader Warning: ' ) ; console . warn . apply ( console , args ) ; } } param directory { string } returns { Promise < boolean | FileError > } private cacheDirectoryExists ( directory ) { return this . file . checkDir ( directory , this . config . cacheDirectoryName ) ; } param replace { boolean } returns { Promise < DirectoryEntry | FileError > } private createCacheDirectory ( replace = false ) { let cacheDirectoryPromise , tempDirectoryPromise ; if ( replace ) { cacheDirectoryPromise = this . file . createDir ( this . getFileCacheDirectory ( ) , this . config . cacheDirectoryName , replace ) ; } else { cacheDirectoryPromise = this . cacheDirectoryExists ( this . getFileCacheDirectory ( ) ) . catch ( ( ) => this . file . createDir ( this . getFileCacheDirectory ( ) , this . config . cacheDirectoryName , false ) ) ; } if ( this . isWKWebView && ! this . isIonicWKWebView ) { if ( replace ) { tempDirectoryPromise = this . file . createDir ( this . file . tempDirectory , this . config . cacheDirectoryName , replace ) ; } else { tempDirectoryPromise = this . cacheDirectoryExists ( this . file . tempDirectory ) . catch ( ( ) => this . file . createDir ( this . file . tempDirectory , this . config . cacheDirectoryName , false ) ) ; } } else { tempDirectoryPromise = Promise . resolve ( ) ; } return Promise . all ( [ cacheDirectoryPromise , tempDirectoryPromise ] ) ; } param { string } url returns { string } private createFileName ( url ) { return ( this . hashString ( url ) . toString ( ) + ( this . config . fileNameCachedWithExtension ? this . getExtensionFromUrl ( url ) : '' ) ) ; } param { string } string returns { number } private hashString ( string ) { let hash = 0 , char ; if ( string . length === 0 ) { return hash ; } for ( let i = 0 ; i < string . length ; i ++ ) { char = string . charCodeAt ( i ) ; hash = ( hash << 5 ) - hash + char ; hash = hash & hash ; } return hash ; } param { string } url returns { string } private getExtensionFromUrl ( url ) { const urlWitoutParams = url . split ( /\#|\?/ ) [ 0 ] ; return ( urlWitoutParams . substr ( ( ~ - urlWitoutParams . lastIndexOf ( '.' ) >>> 0 ) + 1 ) || this . config . fallbackFileNameCachedExtension ) ; } }