diff --git a/src/directives/img-loader.ts b/src/directives/img-loader.ts index 5eaf1f8..fd14b2c 100644 --- a/src/directives/img-loader.ts +++ b/src/directives/img-loader.ts @@ -1,64 +1,76 @@ import { Directive, Input, Output, EventEmitter, ElementRef, Renderer } from '@angular/core'; -import {ImageLoaderConfig} from "../providers/image-loader-config"; +import { ImageLoaderConfig } from "../providers/image-loader-config"; +import {ImageLoader} from "../providers/image-loader"; @Directive({ selector: '[imgLoader]' }) export class ImgLoader { - /** - * The URL of the image to load. - */ - @Input('imgLoader') imageUrl: string; + /** + * The URL of the image to load. + */ + @Input('imgLoader') imageUrl: string; - /** - * The URL of the image to show if an error occurs. Leave this blank to not show anything. - */ - @Input() errorImage: string; + /** + * The URL of the image to show if an error occurs. Leave this blank to not show anything. + */ + @Input() errorImage: string; - /** - * The name of the Ionic Spinner to show while loading. Leave this blank to not show anything. - */ - @Input() spinner: string; + /** + * The name of the Ionic Spinner to show while loading. Leave this blank to not show anything. + */ + @Input() spinner: string; - /** - * Event emitter that notifies you when the image is loaded - * @type {EventEmitter} - */ - @Output() onLoad: EventEmitter = new EventEmitter(); + /** + * Event emitter that notifies you when the image is loaded + * @type {EventEmitter} + */ + @Output() onLoad: EventEmitter = new EventEmitter(); - /** - * Event emiter that notifies you when an error occurs, and passes you the error response/message. - * @type {EventEmitter} - */ - @Output() onError: EventEmitter = new EventEmitter(); + /** + * Event emiter that notifies you when an error occurs, and passes you the error response/message. + * @type {EventEmitter} + */ + @Output() onError: EventEmitter = new EventEmitter(); - /** - * The tag name of the element this directive is attached to. - * This is used to determine whether we're on an `img` tag or something else. - */ - private tagName: string; + /** + * The tag name of the element this directive is attached to. + * This is used to determine whether we're on an `img` tag or something else. + */ + private tagName: string; - /** - * Whether the image is still loading - */ - private isLoading: boolean; + /** + * Whether the image is still loading + */ + private isLoading: boolean; - /** - * Whether an error occurred while loading the image - */ - error: boolean; + /** + * Whether an error occurred while loading the image + */ + error: boolean; - constructor( - private element: ElementRef - , private renderer: Renderer - , private config: ImageLoaderConfig - ) { - } + constructor( + private element: ElementRef + , private renderer: Renderer + , private config: ImageLoaderConfig + , private imageLoader: ImageLoader + ) { + } - ngOnInit(): void { - // set tag name - this.tagName = this.element.nativeElement.tagName; - } + ngOnInit(): void { + // set tag name + this.tagName = this.element.nativeElement.tagName; + + // fetch image + this.imageLoader.getImagePath(this.imageUrl) + .then((imageUrl: string) => { + if (this.tagName === 'IMG') { + this.renderer.setElementAttribute(this.element, 'src', imageUrl); + } else { + this.renderer.setElementStyle(this.element, 'background-image', 'url(\'' + imageUrl +'\')'); + } + }); + } } diff --git a/src/providers/image-loader-config.ts b/src/providers/image-loader-config.ts index f785063..456db48 100644 --- a/src/providers/image-loader-config.ts +++ b/src/providers/image-loader-config.ts @@ -3,16 +3,25 @@ import { Injectable } from '@angular/core'; @Injectable() export class ImageLoaderConfig { - isDebug: boolean = false; + debugMode: boolean = false; - cacheDirectoryPath: string = 'image-loader-cache'; + private _cacheDirectoryName: string = 'image-loader-cache'; + + set cacheDirectoryName(name: string) { + name.replace(/\W/g, ''); + this._cacheDirectoryName = name; + } + + get cacheDirectoryName(): string { + return this._cacheDirectoryName; + } enableDebugMode(): void { - this.isDebug = true; + this.debugMode = true; } - setCacheDirectory(path: string): void { - this.cacheDirectoryPath = path; + setCacheDirectoryName(name: string): void { + this.cacheDirectoryName = name; } } diff --git a/src/providers/image-loader.ts b/src/providers/image-loader.ts index 2ea7285..0273e21 100644 --- a/src/providers/image-loader.ts +++ b/src/providers/image-loader.ts @@ -1,84 +1,203 @@ import { Injectable } from '@angular/core'; import { Http } from '@angular/http'; -import { HTTP, File } from 'ionic-native'; +import { HTTP, File, DirectoryEntry, FileError, FileEntry } from 'ionic-native'; import { Platform } from 'ionic-angular'; import { ImageLoaderConfig } from "./image-loader-config"; declare var cordovaHTTP: any; +declare var cordova: any; @Injectable() export class ImageLoader { - private isNativeAvailable: boolean = false; + private isNativeHttpAvailable: boolean = false; + private isCacheReady: boolean = false; - constructor( - private http: Http, - private platform: Platform, - private config: ImageLoaderConfig - ) {} + constructor(private http: Http, + private platform: Platform, + private config: ImageLoaderConfig) { + } ngOnInit(): void { this.platform.ready().then(() => { if (typeof cordovaHTTP !== 'undefined') { - this.isNativeAvailable = true; - } else if (this.config.isDebug) { + this.isNativeHttpAvailable = true; + } else if (this.config.debugMode) { console.info('ImageLoader: Falling back to @angular/http since cordovaHTTP isn\'t available'); } + this.initCache(); }); } - getRawImage(url: string): Promise { - return new Promise((resolve, reject) => { + getImagePath(imageUrl: string): Promise { + if (this.isCacheReady) { + return new Promise((resolve) => { + this.getCachedImagePath(imageUrl) + .then(imagePath => resolve(imagePath)) + .catch(() => { + // image doesn't exist in cache, lets fetch it and save it + this.getRawImage(imageUrl) + .then(image => { + this.cacheImage(image, this.createFileName(imageUrl)) + .then(() => { + this.getCachedImagePath(imageUrl) + .then(imagePath => resolve(imagePath)) + .catch((e) => { + this.throwError(e); + resolve(imageUrl); + }); + }) + .catch((e) => { + this.throwError(e); + resolve(imageUrl); + }); + }) + .catch((e) => { + this.throwError(e); + resolve(imageUrl); + }); + }); + }); + } else { + this.throwWarning('The cache system is not running. Images will be loaded by your browser instead.'); + return Promise.resolve(imageUrl); + } + } - if (this.isNativeAvailable) { - // cordovaHTTP is available, lets get the image via background thread + private initCache(replace?: boolean): void { + if (!this.filePluginExists) { + return; + } + + this.cacheDirectoryExists + .then((exists: boolean) => { + if (exists) { + this.isCacheReady = true; + } else { + this.createCacheDirectory(replace) + .then((dirEntry: DirectoryEntry) => this.isCacheReady = true) + .catch(this.throwError); + } + }) + .catch(this.throwError); + } + private getRawImage(url: string): Promise { + return new Promise((resolve, reject) => { + if (this.isNativeHttpAvailable) { + // cordovaHTTP is available, lets get the image via background thread HTTP.get(url, {}, {}) .then( data => { console.log(data); - }, - error => { - - } + reject ); - } else { // cordovaHTTP isn't available so we'll use @angular/http - this.http.get(url) .subscribe( data => { console.log(data); resolve(data.arrayBuffer()); }, - error => { - - } + reject ); - } - }); } - checkIfImageExistsInCache(imageHash: string): Promise { - return new Promise((resolve, reject) => { + /** + * + * @param image {ArrayBuffer} Image in binary + * @param fileName {string} File name to save as + * @returns {Promise} Promise that resolves with native URL of file + */ + private cacheImage(image: ArrayBuffer, fileName: string): Promise { + return File.writeFile(cordova.file.cacheDirectory + '/' + this.config.cacheDirectoryName, fileName, new Blob([image]), {replace: true}); + } + private getCachedImagePath(url: string): Promise { + return new Promise((resolve, reject) => { + if (!this.isCacheReady) { + return reject(); + } + let fileName = this.createFileName(url); + let dirPath = cordova.file.cacheDirectory + '/' + this.config.cacheDirectoryName; + File.checkFile(dirPath, fileName) + .then((exists: boolean) => { + if (exists) { + + File.resolveLocalFilesystemUrl(dirPath + '/' + fileName) + .then((fileEntry: FileEntry) => { + resolve(fileEntry.nativeURL); + }) + .catch(reject); + + } else { + reject(); + } + }) + .catch(this.throwError); }); } - hashURL(url: string): string { + private throwError(error: any): void { + if (this.config.debugMode) { + console.error('ImageLoader Error', error); + } + } + + private throwWarning(error: any): void { + if (this.config.debugMode) { + console.warn('ImageLoader Warning', error); + } + } + + private get filePluginExists(): boolean { + if (!cordova || !cordova.file) { + this.throwWarning('Unable to find the cordova file plugin. ImageLoader will not cache images.'); + return false; + } + return true; + } + + private get cacheDirectoryExists(): Promise { + return File.checkDir(cordova.file.cacheDirectory, this.config.cacheDirectoryName); + } + + private createCacheDirectory(replace: boolean = false): Promise { + return File.createDir(cordova.file.cacheDirectory, this.config.cacheDirectoryName, replace); + } + + /** + * Creates a unique file name out of the URL + * @param url {string} URL of the file + * @returns {string} Unique file name + */ + private createFileName(url: string): string { + // get file extension and clean up anything after the extension + let ext: string = url.split('.').pop().split(/\#|\?/)[0]; + // hash the url to get a unique file name + let hash = this.hashString(url); + return hash + '.' + ext; + } + + /** + * Converts a string to a unique 32-bit int + * @param string {string} string to hash + * @returns {number} 32-bit int + */ + private hashString(string: string): number { let hash = 0; let char; - if (url.length === 0) return hash.toString(); - for (let i = 0; i < url.length; i++) { - char = url.charCodeAt(i); - hash = ((hash<<5)-hash)+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.toString(); + return hash; } }