diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..b43bf86b5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +README.md diff --git a/.prettierrc b/.prettierrc index 1d4c3eff7..635e22152 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,8 +1,9 @@ { - "requirePragma": true, "singleQuote": true, "trailingComma": "all", "bracketSpacing": false, "jsxBracketSameLine": true, - "parser": "flow" -} \ No newline at end of file + "overrides": [ + {"files": ["*.js"], "options": {"parser": "flow", "requirePragma": true}} + ] +} diff --git a/README.md b/README.md index 72392ae8c..bb3c9670b 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ Returns a Promise with photo identifier objects from the local camera roll of th | params | object | Yes | Expects a params with the shape described below. | * `first` : {number} : The number of photos wanted in reverse order of the photo application (i.e. most recent first for SavedPhotos). Required. -* `after` : {string} : A cursor that matches `page_info { end_cursor }` returned from a previous call to `getPhotos`. +* `after` : {string} : A cursor that matches `page_info { end_cursor }` returned from a previous call to `getPhotos`. Note that using this will reduce performance slightly on iOS. An alternative is just using the `fromTime` and `toTime` filters, which have no such impact. * `groupTypes` : {string} : Specifies which group types to filter the results to. Valid values are: * `Album` * `All` // default @@ -165,9 +165,13 @@ Returns a Promise with photo identifier objects from the local camera roll of th * `All` * `Videos` * `Photos` // default -* `mimeTypes` : {Array} : Filter by mimetype (e.g. image/jpeg). -* `fromTime` : {timestamp} : Filter from date added. -* `toTime` : {timestamp} : Filter to date added. +* `mimeTypes` : {Array} : Filter by mimetype (e.g. image/jpeg). Note that using this will reduce performance slightly on iOS. +* `fromTime` : {number} : Filter by creation time with a timestamp in milliseconds. This time is exclusive, so we'll select all photos with `timestamp > fromTime`. +* `toTime` : {number} : Filter by creation time with a timestamp in milliseconds. This time is inclusive, so we'll select all photos with `timestamp <= toTime`. +* `include` : {Array} : Whether to include some fields that are slower to fetch + * `filename` : Ensures `image.filename` is available in each node. This has a large performance impact on iOS. + * `fileSize` : Ensures `image.fileSize` is available in each node. This has a large performance impact on iOS. + * `location`: Ensures `location` is available in each node. This has a large performance impact on Android. Returns a Promise which when resolved will be of the following shape: @@ -177,14 +181,14 @@ Returns a Promise which when resolved will be of the following shape: * `group_name`: {string} * `image`: {object} : An object with the following shape: * `uri`: {string} - * `filename`: {string} + * `filename`: {string | null} : Only set if the `include` parameter contains `filename`. * `height`: {number} * `width`: {number} - * `fileSize`: {number} + * `fileSize`: {number | null} : Only set if the `include` parameter contains `fileSize`. * `isStored`: {boolean} * `playableDuration`: {number} - * `timestamp`: {number} - * `location`: {object} : An object with the following shape: + * `timestamp`: {number} : Timestamp in seconds. + * `location`: {object | null} : Only set if the `include` parameter contains `location`. An object with the following shape: * `latitude`: {number} * `longitude`: {number} * `altitude`: {number} @@ -233,8 +237,10 @@ render() { ); } -``` +``` + --- + ### `deletePhotos()` ```javascript diff --git a/android/src/main/java/com/reactnativecommunity/cameraroll/CameraRollModule.java b/android/src/main/java/com/reactnativecommunity/cameraroll/CameraRollModule.java index b2f537367..76f2bc437 100644 --- a/android/src/main/java/com/reactnativecommunity/cameraroll/CameraRollModule.java +++ b/android/src/main/java/com/reactnativecommunity/cameraroll/CameraRollModule.java @@ -47,9 +47,11 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.FileChannel; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.HashMap; +import java.util.Set; import javax.annotation.Nullable; @@ -72,6 +74,10 @@ public class CameraRollModule extends ReactContextBaseJavaModule { private static final String ASSET_TYPE_VIDEOS = "Videos"; private static final String ASSET_TYPE_ALL = "All"; + private static final String INCLUDE_FILENAME = "filename"; + private static final String INCLUDE_FILE_SIZE = "fileSize"; + private static final String INCLUDE_LOCATION = "location"; + private static final String[] PROJECTION = { Images.Media._ID, Images.Media.MIME_TYPE, @@ -247,6 +253,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule { ReadableArray mimeTypes = params.hasKey("mimeTypes") ? params.getArray("mimeTypes") : null; + ReadableArray include = params.hasKey("include") ? params.getArray("include") : null; new GetMediaTask( getReactApplicationContext(), @@ -257,6 +264,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule { assetType, fromTime, toTime, + include, promise) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @@ -271,6 +279,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule { private final String mAssetType; private final long mFromTime; private final long mToTime; + private final Set mInclude; private GetMediaTask( ReactContext context, @@ -281,6 +290,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule { String assetType, long fromTime, long toTime, + @Nullable ReadableArray include, Promise promise) { super(context); mContext = context; @@ -292,6 +302,24 @@ public class CameraRollModule extends ReactContextBaseJavaModule { mAssetType = assetType; mFromTime = fromTime; mToTime = toTime; + mInclude = createSetFromIncludeArray(include); + } + + private static Set createSetFromIncludeArray(@Nullable ReadableArray includeArray) { + Set includeSet = new HashSet<>(); + + if (includeArray == null) { + return includeSet; + } + + for (int i = 0; i < includeArray.size(); i++) { + @Nullable String includeItem = includeArray.getString(i); + if (includeItem != null) { + includeSet.add(includeItem); + } + } + + return includeSet; } @Override @@ -362,7 +390,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule { mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get media"); } else { try { - putEdges(resolver, media, response, mFirst); + putEdges(resolver, media, response, mFirst, mInclude); putPageInfo(media, response, mFirst, !TextUtils.isEmpty(mAfter) ? Integer.parseInt(mAfter) : 0); } finally { media.close(); @@ -463,10 +491,10 @@ public class CameraRollModule extends ReactContextBaseJavaModule { ContentResolver resolver, Cursor media, WritableMap response, - int limit) { + int limit, + Set include) { WritableArray edges = new WritableNativeArray(); media.moveToFirst(); - int idIndex = media.getColumnIndex(Images.Media._ID); int mimeTypeIndex = media.getColumnIndex(Images.Media.MIME_TYPE); int groupNameIndex = media.getColumnIndex(Images.Media.BUCKET_DISPLAY_NAME); int dateTakenIndex = media.getColumnIndex(Images.Media.DATE_TAKEN); @@ -475,14 +503,19 @@ public class CameraRollModule extends ReactContextBaseJavaModule { int sizeIndex = media.getColumnIndex(MediaStore.MediaColumns.SIZE); int dataIndex = media.getColumnIndex(MediaStore.MediaColumns.DATA); + boolean includeLocation = include.contains(INCLUDE_LOCATION); + boolean includeFilename = include.contains(INCLUDE_FILENAME); + boolean includeFileSize = include.contains(INCLUDE_FILE_SIZE); + for (int i = 0; i < limit && !media.isAfterLast(); i++) { WritableMap edge = new WritableNativeMap(); WritableMap node = new WritableNativeMap(); boolean imageInfoSuccess = - putImageInfo(resolver, media, node, idIndex, widthIndex, heightIndex, sizeIndex, dataIndex, mimeTypeIndex); + putImageInfo(resolver, media, node, widthIndex, heightIndex, sizeIndex, dataIndex, + mimeTypeIndex, includeFilename, includeFileSize); if (imageInfoSuccess) { putBasicNodeInfo(media, node, mimeTypeIndex, groupNameIndex, dateTakenIndex); - putLocationInfo(media, node, dataIndex); + putLocationInfo(media, node, dataIndex, includeLocation); edge.putMap("node", node); edges.pushMap(edge); @@ -511,22 +544,18 @@ public class CameraRollModule extends ReactContextBaseJavaModule { ContentResolver resolver, Cursor media, WritableMap node, - int idIndex, int widthIndex, int heightIndex, int sizeIndex, int dataIndex, - int mimeTypeIndex) { + int mimeTypeIndex, + boolean includeFilename, + boolean includeFileSize) { WritableMap image = new WritableNativeMap(); Uri photoUri = Uri.parse("file://" + media.getString(dataIndex)); - File file = new File(media.getString(dataIndex)); - String strFileName = file.getName(); image.putString("uri", photoUri.toString()); - image.putString("filename", strFileName); float width = media.getInt(widthIndex); float height = media.getInt(heightIndex); - long fileSize = media.getLong(sizeIndex); - String mimeType = media.getString(mimeTypeIndex); if (mimeType != null @@ -585,7 +614,21 @@ public class CameraRollModule extends ReactContextBaseJavaModule { } image.putDouble("width", width); image.putDouble("height", height); - image.putDouble("fileSize", fileSize); + + if (includeFilename) { + File file = new File(media.getString(dataIndex)); + String strFileName = file.getName(); + image.putString("filename", strFileName); + } else { + image.putNull("filename"); + } + + if (includeFileSize) { + image.putDouble("fileSize", media.getLong(sizeIndex)); + } else { + image.putNull("fileSize"); + } + node.putMap("image", image); return true; @@ -594,7 +637,13 @@ public class CameraRollModule extends ReactContextBaseJavaModule { private static void putLocationInfo( Cursor media, WritableMap node, - int dataIndex) { + int dataIndex, + boolean includeLocation) { + if (!includeLocation) { + node.putNull("location"); + return; + } + try { // location details are no longer indexed for privacy reasons using string Media.LATITUDE, Media.LONGITUDE // we manually obtain location metadata using ExifInterface#getLatLong(float[]). diff --git a/example/App.js b/example/App.js deleted file mode 100644 index e0944c799..000000000 --- a/example/App.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @flow - */ -import React, {Component} from 'react'; -import {StyleSheet, View} from 'react-native'; -import CameraRollExample from './js/CameraRollExample'; - -type Props = {}; -export default class App extends Component { - render() { - return ( - - - - ); - } -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: '#F5FCFF', - }, -}); diff --git a/example/App.tsx b/example/App.tsx new file mode 100644 index 000000000..1d655c8c3 --- /dev/null +++ b/example/App.tsx @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ +import React, {Component} from 'react'; +import ExampleContainer from './js/ExampleContainer'; + +export default class App extends Component { + render() { + return ; + } +} diff --git a/example/index.js b/example/index.js index f62137809..4b5be70bc 100644 --- a/example/index.js +++ b/example/index.js @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow */ import {AppRegistry} from 'react-native'; import App from './App'; diff --git a/example/js/CameraRollExample.js b/example/js/CameraRollExample.js index acbc980d3..02c88ae9b 100644 --- a/example/js/CameraRollExample.js +++ b/example/js/CameraRollExample.js @@ -57,7 +57,7 @@ export default class CameraRollExample extends React.Component { render() { return ( - + { onPress={this.loadAsset.bind(this, asset)}> - + {asset.node.image.uri} {locationStr} {asset.node.group_name} @@ -135,7 +135,6 @@ export default class CameraRollExample extends React.Component { const styles = StyleSheet.create({ header: { - marginTop: 44, padding: 20, width: Dimensions.get('window').width, }, @@ -150,7 +149,7 @@ const styles = StyleSheet.create({ image: { margin: 4, }, - info: { + flex1: { flex: 1, }, }); diff --git a/example/js/ExampleContainer.tsx b/example/js/ExampleContainer.tsx new file mode 100644 index 000000000..1a42ed56f --- /dev/null +++ b/example/js/ExampleContainer.tsx @@ -0,0 +1,104 @@ +import * as React from 'react'; +import { + SafeAreaView, + StyleSheet, + View, + Button, + Modal, + TouchableWithoutFeedback, +} from 'react-native'; +// @ts-ignore: CameraRollExample has no typings in same folder +import CameraRollExample from './CameraRollExample'; +import GetPhotosPerformanceExample from './GetPhotosPerformanceExample'; + +interface Props {} + +interface State { + showChangeExampleModal: boolean; + currentExampleIndex: number; +} + +interface Example { + label: string; + Component: React.ComponentType; +} + +const examples: Example[] = [ + { + label: 'CameraRollExample', + Component: CameraRollExample, + }, + { + label: 'GetPhotosPerformanceExample', + Component: GetPhotosPerformanceExample, + }, +]; + +/** + * Container for displaying and switching between multiple examples. + * + * Shows a button which opens up a Modal to switch between examples, as well + * as the current example itself. + */ +export default class ExamplesContainer extends React.Component { + state: State = {showChangeExampleModal: false, currentExampleIndex: 0}; + + render() { + const {currentExampleIndex} = this.state; + return ( + +