UNPKG

ionic-image-loader

Version:

Ionic Component and Service to load images in a background thread and cache them for later use

893 lines 35.3 kB
import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { File } 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'; var ImageLoader = (function () { function ImageLoader(config, file, http, platform) { var _this = this; this.config = config; this.file = file; this.http = http; this.platform = platform; /** * Indicates if the cache service is ready. * When the cache service isn't ready, images are loaded via browser instead. * @type {boolean} */ this.isCacheReady = false; /** * Indicates if this service is initialized. * This service is initialized once all the setup is done. * @type {boolean} */ this.isInit = false; /** * Number of concurrent requests allowed * @type {number} */ this.concurrency = 5; /** * Queue items * @type {Array} */ this.queue = []; this.processing = 0; /** * Fast accessible Object for currently processing items */ this.currentlyProcessing = {}; this.cacheIndex = []; this.currentCacheSize = 0; this.indexed = false; if (!platform.is('cordova')) { // we are running on a browser, or using livereload // plugin will not function in this case 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(function (res) { if (_this.nativeAvailable) { _this.initCache(); } else { // we are running on a browser, or using livereload // plugin will not function in this case // we are running on a browser, or using livereload // plugin will not function in this case _this.isInit = true; _this.throwWarning('You are running on a browser or using livereload, IonicImageLoader will not function, falling back to browser loading.'); } }); } } Object.defineProperty(ImageLoader.prototype, "nativeAvailable", { get: function () { return File.installed(); }, enumerable: true, configurable: true }); Object.defineProperty(ImageLoader.prototype, "isCacheSpaceExceeded", { get: function () { return (this.config.maxCacheSize > -1 && this.currentCacheSize > this.config.maxCacheSize); }, enumerable: true, configurable: true }); Object.defineProperty(ImageLoader.prototype, "isWKWebView", { get: function () { return (this.platform.is('ios') && window.webkit && window.webkit.messageHandlers); }, enumerable: true, configurable: true }); Object.defineProperty(ImageLoader.prototype, "isIonicWKWebView", { get: function () { return ((this.isWKWebView || this.platform.is('android')) && (location.host === 'localhost:8080' || window.LiveReload)); }, enumerable: true, configurable: true }); Object.defineProperty(ImageLoader.prototype, "isDevServer", { get: function () { return window['IonicDevServer'] !== undefined; }, enumerable: true, configurable: true }); Object.defineProperty(ImageLoader.prototype, "canProcess", { /** * Check if we can process more items in the queue * @returns {boolean} */ get: /** * Check if we can process more items in the queue * @returns {boolean} */ function () { return this.queue.length > 0 && this.processing < this.concurrency; }, enumerable: true, configurable: true }); /** * Preload an image * @param {string} imageUrl Image URL * @returns {Promise<string>} returns a promise that resolves with the cached image URL */ /** * Preload an image * @param {string} imageUrl Image URL * @returns {Promise<string>} returns a promise that resolves with the cached image URL */ ImageLoader.prototype.preload = /** * Preload an image * @param {string} imageUrl Image URL * @returns {Promise<string>} returns a promise that resolves with the cached image URL */ function (imageUrl) { return this.getImagePath(imageUrl); }; ImageLoader.prototype.getFileCacheDirectory = function () { if (this.config.cacheDirectoryType == 'data') { return this.file.dataDirectory; } return this.file.cacheDirectory; }; /** * Clears cache of a single image * @param {string} imageUrl Image URL */ /** * Clears cache of a single image * @param {string} imageUrl Image URL */ ImageLoader.prototype.clearImageCache = /** * Clears cache of a single image * @param {string} imageUrl Image URL */ function (imageUrl) { var _this = this; if (!this.platform.is('cordova')) { return; } var clear = function () { if (!_this.isInit) { // do not run this method until our service is initialized setTimeout(clear.bind(_this), 500); return; } var fileName = _this.createFileName(imageUrl); var route = _this.getFileCacheDirectory() + _this.config.cacheDirectoryName; // pause any operations // pause any operations _this.isInit = false; _this.file.removeFile(route, fileName) .then(function () { if (_this.isWKWebView && !_this.isIonicWKWebView) { _this.file.removeFile(_this.file.tempDirectory + _this.config.cacheDirectoryName, fileName) .then(function () { _this.initCache(true); }).catch(function (err) { // Handle error? }); } else { _this.initCache(true); } }).catch(_this.throwError.bind(_this)); }; clear(); }; /** * Clears the cache */ /** * Clears the cache */ ImageLoader.prototype.clearCache = /** * Clears the cache */ function () { var _this = this; if (!this.platform.is('cordova')) { return; } var clear = function () { if (!_this.isInit) { // do not run this method until our service is initialized setTimeout(clear.bind(_this), 500); return; } // pause any operations // pause any operations _this.isInit = false; _this.file.removeRecursively(_this.getFileCacheDirectory(), _this.config.cacheDirectoryName) .then(function () { if (_this.isWKWebView && !_this.isIonicWKWebView) { // also clear the temp files // also clear the temp files _this.file .removeRecursively(_this.file.tempDirectory, _this.config.cacheDirectoryName) .catch(function (error) { // Noop catch. Removing the tempDirectory might fail, // as it is not persistent. }) .then(function () { _this.initCache(true); }); } else { _this.initCache(true); } }) .catch(_this.throwError.bind(_this)); }; clear(); }; /** * Gets the filesystem path of an image. * This will return the remote path if anything goes wrong or if the cache service isn't ready yet. * @param {string} imageUrl The remote URL of the image * @returns {Promise<string>} Returns a promise that will always resolve with an image URL */ /** * Gets the filesystem path of an image. * This will return the remote path if anything goes wrong or if the cache service isn't ready yet. * @param {string} imageUrl The remote URL of the image * @returns {Promise<string>} Returns a promise that will always resolve with an image URL */ ImageLoader.prototype.getImagePath = /** * Gets the filesystem path of an image. * This will return the remote path if anything goes wrong or if the cache service isn't ready yet. * @param {string} imageUrl The remote URL of the image * @returns {Promise<string>} Returns a promise that will always resolve with an image URL */ function (imageUrl) { var _this = this; if (typeof imageUrl !== 'string' || imageUrl.length <= 0) { return Promise.reject('The image url provided was empty or invalid.'); } return new Promise(function (resolve, reject) { var getImage = function () { if (_this.isImageUrlRelative(imageUrl)) { resolve(imageUrl); } else { _this.getCachedImagePath(imageUrl) .then(resolve) .catch(function () { // image doesn't exist in cache, lets fetch it and save it // image doesn't exist in cache, lets fetch it and save it _this.addItemToQueue(imageUrl, resolve, reject); }); } }; var check = function () { 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(function () { return check(); }, 250); } }; check(); }); }; /** * Returns if an imageUrl is an relative path * @param {string} imageUrl */ /** * Returns if an imageUrl is an relative path * @param {string} imageUrl */ ImageLoader.prototype.isImageUrlRelative = /** * Returns if an imageUrl is an relative path * @param {string} imageUrl */ function (imageUrl) { return !/^(https?|file):\/\/\/?/i.test(imageUrl); }; /** * Add an item to the queue * @param {string} imageUrl * @param resolve * @param reject */ /** * Add an item to the queue * @param {string} imageUrl * @param resolve * @param reject */ ImageLoader.prototype.addItemToQueue = /** * Add an item to the queue * @param {string} imageUrl * @param resolve * @param reject */ function (imageUrl, resolve, reject) { this.queue.push({ imageUrl: imageUrl, resolve: resolve, reject: reject, }); this.processQueue(); }; /** * Processes one item from the queue */ /** * Processes one item from the queue */ ImageLoader.prototype.processQueue = /** * Processes one item from the queue */ function () { var _this = this; // make sure we can process items first if (!this.canProcess) { return; } // increase the processing number this.processing++; // take the first item from queue var currentItem = this.queue.splice(0, 1)[0]; // function to call when done processing this item // this will reduce the processing number // then will execute this function again to process any remaining items var done = function () { _this.processing--; _this.processQueue(); // only delete if it's the last/unique occurrence in the queue if (_this.currentlyProcessing[currentItem.imageUrl] !== undefined && !_this.currentlyInQueue(currentItem.imageUrl)) { delete _this.currentlyProcessing[currentItem.imageUrl]; } }; var error = function (e) { currentItem.reject(); _this.throwError(e); done(); }; if (this.currentlyProcessing[currentItem.imageUrl] === undefined) { this.currentlyProcessing[currentItem.imageUrl] = new Promise(function (resolve, reject) { // process more items concurrently if we can if (_this.canProcess) { _this.processQueue(); } var localDir = _this.getFileCacheDirectory() + _this.config.cacheDirectoryName + '/'; var fileName = _this.createFileName(currentItem.imageUrl); _this.http.get(currentItem.imageUrl, { responseType: 'blob', headers: _this.config.httpHeaders }).subscribe(function (data) { _this.file.writeFile(localDir, fileName, data, { replace: true }).then(function (file) { if (_this.isCacheSpaceExceeded) { _this.maintainCacheSize(); } _this.addFileToIndex(file).then(function () { _this.getCachedImagePath(currentItem.imageUrl).then(function (localUrl) { currentItem.resolve(localUrl); resolve(); done(); _this.maintainCacheSize(); }); }); }).catch(function (e) { // Could not write image error(e); reject(e); }); }, function (e) { // Could not get image via httpClient error(e); reject(e); }); }); } else { // Prevented same Image from loading at the same time this.currentlyProcessing[currentItem.imageUrl].then(function () { _this.getCachedImagePath(currentItem.imageUrl).then(function (localUrl) { currentItem.resolve(localUrl); }); done(); }, function (e) { error(e); }); } }; /** * Search if the url is currently in the queue * @param imageUrl {string} Image url to search * @returns {boolean} */ /** * Search if the url is currently in the queue * @param imageUrl {string} Image url to search * @returns {boolean} */ ImageLoader.prototype.currentlyInQueue = /** * Search if the url is currently in the queue * @param imageUrl {string} Image url to search * @returns {boolean} */ function (imageUrl) { return this.queue.some(function (item) { return item.imageUrl === imageUrl; }); }; /** * Initialize the cache service * @param [boolean] replace Whether to replace the cache directory if it already exists */ /** * Initialize the cache service * @param [boolean] replace Whether to replace the cache directory if it already exists */ ImageLoader.prototype.initCache = /** * Initialize the cache service * @param [boolean] replace Whether to replace the cache directory if it already exists */ function (replace) { var _this = this; this.concurrency = this.config.concurrency; // create cache directories if they do not exist this.createCacheDirectory(replace) .catch(function (e) { _this.throwError(e); _this.isInit = true; }) .then(function () { return _this.indexCache(); }) .then(function () { _this.isCacheReady = true; _this.isInit = true; }); }; /** * Adds a file to index. * Also deletes any files if they are older than the set maximum cache age. * @param {FileEntry} file File to index * @returns {Promise<any>} */ /** * Adds a file to index. * Also deletes any files if they are older than the set maximum cache age. * @param {FileEntry} file File to index * @returns {Promise<any>} */ ImageLoader.prototype.addFileToIndex = /** * Adds a file to index. * Also deletes any files if they are older than the set maximum cache age. * @param {FileEntry} file File to index * @returns {Promise<any>} */ function (file) { var _this = this; return new Promise(function (resolve, reject) { return file.getMetadata(resolve, reject); }).then(function (metadata) { if (_this.config.maxCacheAge > -1 && Date.now() - metadata.modificationTime.getTime() > _this.config.maxCacheAge) { // file age exceeds maximum cache age return _this.removeFile(file.name); } else { // file age doesn't exceed maximum cache age, or maximum cache age isn't set // file age doesn't exceed maximum cache age, or maximum cache age isn't set _this.currentCacheSize += metadata.size; // add item to index // add item to index _this.cacheIndex.push({ name: file.name, modificationTime: metadata.modificationTime, size: metadata.size, }); return Promise.resolve(); } }); }; /** * Indexes the cache if necessary * @returns {Promise<void>} */ /** * Indexes the cache if necessary * @returns {Promise<void>} */ ImageLoader.prototype.indexCache = /** * Indexes the cache if necessary * @returns {Promise<void>} */ function () { var _this = this; this.cacheIndex = []; return this.file.listDir(this.getFileCacheDirectory(), this.config.cacheDirectoryName) .then(function (files) { return Promise.all(files.map(_this.addFileToIndex.bind(_this))); }) .then(function () { // Sort items by date. Most recent to oldest. // Sort items by date. Most recent to oldest. _this.cacheIndex = _this.cacheIndex.sort(function (a, b) { return (a > b ? -1 : a < b ? 1 : 0); }); _this.indexed = true; return Promise.resolve(); }) .catch(function (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. */ /** * 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. */ ImageLoader.prototype.maintainCacheSize = /** * 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. */ function () { var _this = this; if (this.config.maxCacheSize > -1 && this.indexed) { var maintain_1 = function () { if (_this.currentCacheSize > _this.config.maxCacheSize) { // called when item is done processing var next_1 = function () { _this.currentCacheSize -= file_1.size; maintain_1(); }; // grab the first item in index since it's the oldest one var file_1 = _this.cacheIndex.splice(0, 1)[0]; if (typeof file_1 === 'undefined') { return maintain_1(); } // delete the file then process next file if necessary // delete the file then process next file if necessary _this.removeFile(file_1.name) .then(function () { return next_1(); }) .catch(function () { return next_1(); }); // ignore errors, nothing we can do about it } }; maintain_1(); } }; /** * Remove a file * @param {string} file The name of the file to remove * @returns {Promise<any>} */ /** * Remove a file * @param {string} file The name of the file to remove * @returns {Promise<any>} */ ImageLoader.prototype.removeFile = /** * Remove a file * @param {string} file The name of the file to remove * @returns {Promise<any>} */ function (file) { var _this = this; return this.file .removeFile(this.getFileCacheDirectory() + this.config.cacheDirectoryName, file) .then(function () { if (_this.isWKWebView && !_this.isIonicWKWebView) { return _this.file .removeFile(_this.file.tempDirectory + _this.config.cacheDirectoryName, file) .catch(function () { // Noop catch. Removing the files from tempDirectory might fail, as it is not persistent. }); } }); }; /** * Get the local path of a previously cached image if exists * @param {string} url The remote URL of the image * @returns {Promise<string>} Returns a promise that resolves with the local path if exists, or rejects if doesn't exist */ /** * Get the local path of a previously cached image if exists * @param {string} url The remote URL of the image * @returns {Promise<string>} Returns a promise that resolves with the local path if exists, or rejects if doesn't exist */ ImageLoader.prototype.getCachedImagePath = /** * Get the local path of a previously cached image if exists * @param {string} url The remote URL of the image * @returns {Promise<string>} Returns a promise that resolves with the local path if exists, or rejects if doesn't exist */ function (url) { var _this = this; return new Promise(function (resolve, reject) { // make sure cache is ready if (!_this.isCacheReady) { return reject(); } // if we're running with livereload, ignore cache and call the resource from it's URL if (_this.isDevServer) { return resolve(url); } // get file name var fileName = _this.createFileName(url); // get full path var dirPath = _this.getFileCacheDirectory() + _this.config.cacheDirectoryName, tempDirPath = _this.file.tempDirectory + _this.config.cacheDirectoryName; // check if exists // check if exists _this.file .resolveLocalFilesystemUrl(dirPath + '/' + fileName) .then(function (fileEntry) { // file exists in cache if (_this.config.imageReturnType === 'base64') { // read the file as data url and return the base64 string. // should always be successful as the existence of the file // is already ensured // read the file as data url and return the base64 string. // should always be successful as the existence of the file // is already ensured _this.file .readAsDataURL(dirPath, fileName) .then(function (base64) { base64 = base64.replace('data:null', 'data:*/*'); resolve(base64); }) .catch(reject); } else if (_this.config.imageReturnType === 'uri') { // now check if iOS device & using WKWebView Engine. // in this case only the tempDirectory is accessible, // therefore the file needs to be copied into that directory first! if (_this.isIonicWKWebView) { // Use Ionic convertFileSrc to generate the right URL for Ionic WKWebView resolve(Ionic.WebView.convertFileSrc(fileEntry.nativeURL)); } else if (_this.isWKWebView) { // check if file already exists in temp directory // check if file already exists in temp directory _this.file .resolveLocalFilesystemUrl(tempDirPath + '/' + fileName) .then(function (tempFileEntry) { // file exists in temp directory // return native path resolve(Ionic.WebView.convertFileSrc(tempFileEntry.nativeURL)); }) .catch(function () { // file does not yet exist in the temp directory. // copy it! // file does not yet exist in the temp directory. // copy it! _this.file .copyFile(dirPath, fileName, tempDirPath, fileName) .then(function (tempFileEntry) { // now the file exists in the temp directory // return native path resolve(Ionic.WebView.convertFileSrc(tempFileEntry.nativeURL)); }) .catch(reject); }); } else { // return native path resolve(Ionic.WebView.convertFileSrc(fileEntry.nativeURL)); } } }) .catch(reject); // file doesn't exist }); }; /** * Throws a console error if debug mode is enabled * @param {any[]} args Error message */ /** * Throws a console error if debug mode is enabled * @param {any[]} args Error message */ ImageLoader.prototype.throwError = /** * Throws a console error if debug mode is enabled * @param {any[]} args Error message */ function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } if (this.config.debugMode) { args.unshift('ImageLoader Error: '); console.error.apply(console, args); } }; /** * Throws a console warning if debug mode is enabled * @param {any[]} args Error message */ /** * Throws a console warning if debug mode is enabled * @param {any[]} args Error message */ ImageLoader.prototype.throwWarning = /** * Throws a console warning if debug mode is enabled * @param {any[]} args Error message */ function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } if (this.config.debugMode) { args.unshift('ImageLoader Warning: '); console.warn.apply(console, args); } }; /** * Check if the cache directory exists * @param directory {string} The directory to check. Either this.file.tempDirectory or this.getFileCacheDirectory() * @returns {Promise<boolean|FileError>} Returns a promise that resolves if exists, and rejects if it doesn't */ /** * Check if the cache directory exists * @param directory {string} The directory to check. Either this.file.tempDirectory or this.getFileCacheDirectory() * @returns {Promise<boolean|FileError>} Returns a promise that resolves if exists, and rejects if it doesn't */ ImageLoader.prototype.cacheDirectoryExists = /** * Check if the cache directory exists * @param directory {string} The directory to check. Either this.file.tempDirectory or this.getFileCacheDirectory() * @returns {Promise<boolean|FileError>} Returns a promise that resolves if exists, and rejects if it doesn't */ function (directory) { return this.file.checkDir(directory, this.config.cacheDirectoryName); }; /** * Create the cache directories * @param replace {boolean} override directory if exists * @returns {Promise<DirectoryEntry|FileError>} Returns a promise that resolves if the directories were created, and rejects on error */ /** * Create the cache directories * @param replace {boolean} override directory if exists * @returns {Promise<DirectoryEntry|FileError>} Returns a promise that resolves if the directories were created, and rejects on error */ ImageLoader.prototype.createCacheDirectory = /** * Create the cache directories * @param replace {boolean} override directory if exists * @returns {Promise<DirectoryEntry|FileError>} Returns a promise that resolves if the directories were created, and rejects on error */ function (replace) { var _this = this; if (replace === void 0) { replace = false; } var cacheDirectoryPromise, tempDirectoryPromise; if (replace) { // create or replace the cache directory cacheDirectoryPromise = this.file.createDir(this.getFileCacheDirectory(), this.config.cacheDirectoryName, replace); } else { // check if the cache directory exists. // if it does not exist create it! cacheDirectoryPromise = this.cacheDirectoryExists(this.getFileCacheDirectory()) .catch(function () { return _this.file.createDir(_this.getFileCacheDirectory(), _this.config.cacheDirectoryName, false); }); } if (this.isWKWebView && !this.isIonicWKWebView) { if (replace) { // create or replace the temp directory tempDirectoryPromise = this.file.createDir(this.file.tempDirectory, this.config.cacheDirectoryName, replace); } else { // check if the temp directory exists. // if it does not exist create it! tempDirectoryPromise = this.cacheDirectoryExists(this.file.tempDirectory).catch(function () { return _this.file.createDir(_this.file.tempDirectory, _this.config.cacheDirectoryName, false); }); } } else { tempDirectoryPromise = Promise.resolve(); } return Promise.all([cacheDirectoryPromise, tempDirectoryPromise]); }; /** * Creates a unique file name out of the URL * @param {string} url URL of the file * @returns {string} Unique file name */ /** * Creates a unique file name out of the URL * @param {string} url URL of the file * @returns {string} Unique file name */ ImageLoader.prototype.createFileName = /** * Creates a unique file name out of the URL * @param {string} url URL of the file * @returns {string} Unique file name */ function (url) { // hash the url to get a unique file name return (this.hashString(url).toString() + (this.config.fileNameCachedWithExtension ? this.getExtensionFromUrl(url) : '')); }; /** * Converts a string to a unique 32-bit int * @param {string} string string to hash * @returns {number} 32-bit int */ /** * Converts a string to a unique 32-bit int * @param {string} string string to hash * @returns {number} 32-bit int */ ImageLoader.prototype.hashString = /** * Converts a string to a unique 32-bit int * @param {string} string string to hash * @returns {number} 32-bit int */ function (string) { var hash = 0, char; if (string.length === 0) { return hash; } for (var i = 0; i < string.length; i++) { char = string.charCodeAt(i); // tslint:disable-next-line hash = (hash << 5) - hash + char; // tslint:disable-next-line hash = hash & hash; } return hash; }; /** * Extract extension from filename or url * * @param {string} url * @returns {string} */ /** * Extract extension from filename or url * * @param {string} url * @returns {string} */ ImageLoader.prototype.getExtensionFromUrl = /** * Extract extension from filename or url * * @param {string} url * @returns {string} */ function (url) { var urlWitoutParams = url.split(/\#|\?/)[0]; return (urlWitoutParams.substr((~-urlWitoutParams.lastIndexOf('.') >>> 0) + 1) || this.config.fallbackFileNameCachedExtension); }; ImageLoader.decorators = [ { type: Injectable }, ]; /** @nocollapse */ ImageLoader.ctorParameters = function () { return [ { type: ImageLoaderConfig, }, { type: File, }, { type: HttpClient, }, { type: Platform, }, ]; }; return ImageLoader; }()); export { ImageLoader }; //# sourceMappingURL=image-loader.js.map