forked from kfiroo/react-native-cached-image
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
447 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,3 +35,4 @@ jspm_packages | |
|
||
# Optional REPL history | ||
.node_repl_history | ||
/.idea/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
'use strict'; | ||
|
||
const _ = require('lodash'); | ||
const React = require('react'); | ||
const ReactNative = require('react-native'); | ||
|
||
const ImageCacheProvider = require('./ImageCacheProvider'); | ||
|
||
const { | ||
Image, | ||
ActivityIndicator, | ||
NetInfo | ||
} = ReactNative; | ||
|
||
const { | ||
StyleSheet | ||
} = ReactNative; | ||
|
||
const styles = StyleSheet.create({ | ||
image: { | ||
backgroundColor: 'transparent' | ||
}, | ||
loader: { | ||
backgroundColor: 'transparent', | ||
flex: 1 | ||
} | ||
}); | ||
|
||
const CacheableImage = React.createClass({ | ||
propTypes: { | ||
renderImage: React.PropTypes.func.isRequired, | ||
activityIndicatorProps: React.PropTypes.object.isRequired, | ||
defaultSource: Image.propTypes.source, | ||
useQueryParamsInCacheKey: React.PropTypes.oneOfType([ | ||
React.PropTypes.bool, | ||
React.PropTypes.array | ||
]).isRequired | ||
}, | ||
|
||
getDefaultProps() { | ||
return { | ||
renderImage: props => (<Image {...props}/>), | ||
activityIndicatorProps: {}, | ||
useQueryParamsInCacheKey: false | ||
}; | ||
}, | ||
|
||
getInitialState() { | ||
this._isMounted = false; | ||
return { | ||
isRemote: false, | ||
cachedImagePath: null, | ||
jobId: null, | ||
networkAvailable: true | ||
}; | ||
}, | ||
|
||
safeSetState(newState) { | ||
if (!this._isMounted) { | ||
return; | ||
} | ||
return this.setState(newState); | ||
}, | ||
|
||
componentWillMount() { | ||
this._isMounted = true; | ||
NetInfo.isConnected.addEventListener('change', this.handleConnectivityChange); | ||
// initial | ||
NetInfo.isConnected.fetch() | ||
.then(isConnected => { | ||
this.safeSetState({ | ||
networkAvailable: isConnected | ||
}); | ||
}); | ||
|
||
this.processSource(this.props.source); | ||
}, | ||
|
||
componentWillUnmount() { | ||
this._isMounted = false; | ||
NetInfo.isConnected.removeEventListener('change', this.handleConnectivityChange); | ||
}, | ||
|
||
componentWillReceiveProps(nextProps) { | ||
if (this.props.source !== nextProps.source) { | ||
this.processSource(nextProps.source); | ||
} | ||
}, | ||
|
||
handleConnectivityChange(isConnected) { | ||
this.safeSetState({ | ||
networkAvailable: isConnected | ||
}); | ||
}, | ||
|
||
processSource(source) { | ||
const url = _.get(source, ['uri'], null); | ||
if (ImageCacheProvider.isCacheable(url)) { | ||
const options = _.pick(this.props, ['useQueryParamsInCacheKey']); | ||
// try to get the image path from cache | ||
ImageCacheProvider.getCachedImagePath(url, options) | ||
// try to put the image in cache if | ||
.catch(() => ImageCacheProvider.cacheImage(url, options)) | ||
.then(cachedImagePath => { | ||
this.safeSetState({ | ||
cachedImagePath | ||
}); | ||
}) | ||
.catch(err => { | ||
console.log('>>> error ', err); | ||
this.safeSetState({ | ||
cachedImagePath: null | ||
}); | ||
}); | ||
this.safeSetState({ | ||
isRemote: true | ||
}); | ||
} else { | ||
this.safeSetState({ | ||
isRemote: false | ||
}); | ||
} | ||
}, | ||
|
||
render() { | ||
if (!this.state.isRemote) { | ||
return this.renderLocal(); | ||
} | ||
if (this.state.cachedImagePath) { | ||
return this.renderCache(); | ||
} | ||
if (this.props.defaultSource) { | ||
return this.renderDefaultSource(); | ||
} | ||
return this.renderLoader(); | ||
}, | ||
|
||
renderLocal() { | ||
const props = _.omit(this.props, ['defaultSource', 'activityIndicatorProps', 'style']); | ||
const style = this.props.style || styles.image; | ||
return this.props.renderImage({ | ||
...props, | ||
style | ||
}); | ||
}, | ||
|
||
renderCache() { | ||
const props = _.omit(this.props, ['defaultSource', 'activityIndicatorProps', 'style']); | ||
const style = this.props.style || styles.image; | ||
return this.props.renderImage({ | ||
...props, | ||
style, | ||
source: { | ||
uri: 'file://' + this.state.cachedImagePath | ||
} | ||
}); | ||
}, | ||
|
||
renderDefaultSource() { | ||
const {children, defaultSource, ...props} = this.props; | ||
return ( | ||
<CacheableImage {...props} source={defaultSource}> | ||
{children} | ||
</CacheableImage> | ||
); | ||
}, | ||
|
||
renderLoader() { | ||
const props = _.omit(this.props.activityIndicatorProps, ['style']); | ||
const style = [this.props.style, this.props.activityIndicatorProps.style || styles.loader]; | ||
return ( | ||
<ActivityIndicator | ||
{...props} | ||
style={style} | ||
/> | ||
); | ||
} | ||
}); | ||
|
||
module.exports = CacheableImage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
'use strict'; | ||
|
||
const _ = require('lodash'); | ||
const RNFS = require('react-native-fs'); | ||
|
||
const { | ||
DocumentDirectoryPath | ||
} = RNFS; | ||
|
||
const SHA1 = require("crypto-js/sha1"); | ||
const URL = require('url-parse'); | ||
|
||
const defaultOptions = { | ||
useQueryParamsInCacheKey: false | ||
}; | ||
|
||
const activeDownloads = {}; | ||
|
||
function serializeObjectKeys(obj) { | ||
return _(obj) | ||
.toPairs() | ||
.sortBy(a => a[0]) | ||
.map(a => a[1]) | ||
.value(); | ||
} | ||
|
||
function getQueryForCacheKey(url, useQueryParamsInCacheKey) { | ||
if (_.isArray(useQueryParamsInCacheKey)) { | ||
return serializeObjectKeys(_.pick(url.query, useQueryParamsInCacheKey)); | ||
} | ||
if (useQueryParamsInCacheKey) { | ||
return serializeObjectKeys(url.query); | ||
} | ||
return ''; | ||
} | ||
|
||
function generateCacheKey(url, options) { | ||
const parsedUrl = new URL(url, null, true); | ||
const parts = parsedUrl.pathname.split('.'); | ||
const type = parts.length > 1 ? ('.' + parts.pop()) : ''; | ||
const pathname = parts.join('.'); | ||
const cacheable = pathname + getQueryForCacheKey(parsedUrl, options.useQueryParamsInCacheKey); | ||
return SHA1(cacheable) + type; | ||
} | ||
|
||
function getCachePath(url, options) { | ||
if (options.cacheGroup) { | ||
return options.cacheGroup; | ||
} | ||
const parsedUrl = new URL(url); | ||
return parsedUrl.host; | ||
} | ||
|
||
function getCachedImageFilePath(url, options) { | ||
const cacheKey = generateCacheKey(url, options); | ||
const cachePath = getCachePath(url, options); | ||
|
||
const dirPath = DocumentDirectoryPath + '/' + cachePath; | ||
return dirPath + '/' + cacheKey; | ||
} | ||
|
||
function deleteFile(filePath) { | ||
return RNFS.exists(filePath) | ||
.then(res => res && RNFS.unlink(filePath)) | ||
.catch((err) => { | ||
// swallow error to always resolve | ||
}); | ||
} | ||
|
||
function ensurePath(filePath) { | ||
const parts = filePath.split('/'); | ||
const dirPath = _.initial(parts).join('/'); | ||
return RNFS.mkdir(dirPath, {NSURLIsExcludedFromBackupKey: true}); | ||
} | ||
|
||
/** | ||
* returns a promise that is resolved when the download of the requested file | ||
* is complete and the file is saved. | ||
* if the download fails, or was stopped the partial file is deleted, and the | ||
* promise is rejected | ||
* @param fromUrl | ||
* @param toFile | ||
* @returns {Promise} | ||
*/ | ||
function downloadImage(fromUrl, toFile) { | ||
// use toFile as the key as is was created using the cacheKey | ||
if (!_.has(activeDownloads, toFile)) { | ||
// create an active download for this file | ||
activeDownloads[toFile] = new Promise((resolve, reject) => { | ||
const downloadOptions = { | ||
fromUrl, | ||
toFile | ||
}; | ||
RNFS.downloadFile(downloadOptions).promise | ||
.then(() => { | ||
resolve(toFile); | ||
}) | ||
.catch(err => deleteFile(toFile) | ||
.then(() => reject(err)) | ||
) | ||
.finally(() => { | ||
// cleanup | ||
delete activeDownloads[toFile]; | ||
}); | ||
}); | ||
} | ||
return activeDownloads[toFile]; | ||
} | ||
|
||
function createPrefetcer(list) { | ||
const urls = _.clone(list); | ||
return { | ||
next() { | ||
return urls.shift(); | ||
} | ||
}; | ||
} | ||
|
||
function runPrefetchTask(prefetcher, options) { | ||
const url = prefetcher.next(); | ||
if (!url) { | ||
return Promise.resolve(); | ||
} | ||
// if url is cacheable - cache it | ||
if (isCacheable(url)) { | ||
// check cache | ||
return getCachedImagePath(url, options) | ||
// if not found download | ||
.catch(() => cacheImage(url, options)) | ||
// then run next task | ||
.then(() => runPrefetchTask(prefetcher, options)); | ||
} | ||
// else get next | ||
return runPrefetchTask(prefetcher, options); | ||
} | ||
|
||
// API | ||
|
||
function isCacheable(url) { | ||
return _.isString(url) && (_.startsWith(url, 'http://') || _.startsWith(url, 'https://')); | ||
} | ||
|
||
function getCachedImagePath(url, options = defaultOptions) { | ||
const filePath = getCachedImageFilePath(url, options); | ||
return RNFS.stat(filePath) | ||
.then(res => { | ||
if (!res.isFile()) { | ||
// reject the promise if res is not a file | ||
throw new Error('Failed to get image from cache'); | ||
} | ||
return filePath; | ||
}) | ||
.catch(err => { | ||
throw err; | ||
}) | ||
} | ||
|
||
function cacheImage(url, options = defaultOptions) { | ||
const filePath = getCachedImageFilePath(url, options); | ||
return ensurePath(filePath) | ||
.then(() => downloadImage(url, filePath)); | ||
} | ||
|
||
function deleteCachedImage(url, options = defaultOptions) { | ||
const filePath = getCachedImageFilePath(url, options); | ||
return deleteFile(filePath); | ||
} | ||
|
||
function cacheMultipleImages(urls, options = defaultOptions) { | ||
const prefetcher = createPrefetcer(urls); | ||
const numberOfWorkers = urls.length; | ||
const promises = _.times(numberOfWorkers, () => | ||
runPrefetchTask(prefetcher, options) | ||
); | ||
return Promise.all(promises); | ||
} | ||
|
||
function deleteMultipleCachedImages(urls, options = defaultOptions) { | ||
return _.reduce(urls, (p, url) => | ||
p.then(() => deleteCachedImage(url, options)), | ||
Promise.resolve() | ||
); | ||
} | ||
|
||
module.exports = { | ||
isCacheable, | ||
getCachedImagePath, | ||
cacheImage, | ||
deleteCachedImage, | ||
cacheMultipleImages, | ||
deleteMultipleCachedImages | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
'use strict'; | ||
|
||
module.exports = require('./CachedImage'); | ||
module.exports.ImageCacheProvider = require('./ImageCacheProvider'); |
Oops, something went wrong.