feat: Added `include` parameter to getPhotos to let users tradeoff performance by omitting metadata (#178) (#200)
BREAKING CHANGE: meta data is no longer added as default. This applies to `fileName`, `fileSize` and `location`. If you need this metadata use the new `include` parameter. Adding this metadata will slow down the `getPhotos` function, so consider only retrieving this meta data for smaller sets of images as needed, instead of for all images. * Created getPhotosFast function, fixed inconsistent getPhotos toDate logic * Fixed wrong param name, added better typechecking * Added example, renamed allowEmptyFilenames to skipGettingFilenames * Removed `after` from getPhotosFast docs * Redid implementation based on a new `include` param * Updated API to use include parameter * Fixed flow checking by converting index.ts to Typescript * Unformatted README.md * Unformatted README.md * Unformatted README.md * Added .prettierignore and ignored README.md for now * Made example/index.js not checked by Flow * Updated README.md to include notes in the outputs too * Made inclusion of fields consistent, addressed other feedback * Renamed GetPhotosFastParams back to GetPhotosParams * Updated documentation to reflect nullable types * Updated typings and documentation for fromTime, toTime * Updated to fix `hasNextPage` being incorrectly false in some cases Co-authored-by: Harry Yu <hy.harry.yu@gmail.com>
This commit is contained in:
parent
27f277169e
commit
4da8310892
|
@ -0,0 +1 @@
|
||||||
|
README.md
|
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"requirePragma": true,
|
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"bracketSpacing": false,
|
"bracketSpacing": false,
|
||||||
"jsxBracketSameLine": true,
|
"jsxBracketSameLine": true,
|
||||||
"parser": "flow"
|
"overrides": [
|
||||||
|
{"files": ["*.js"], "options": {"parser": "flow", "requirePragma": true}}
|
||||||
|
]
|
||||||
}
|
}
|
22
README.md
22
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. |
|
| 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.
|
* `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:
|
* `groupTypes` : {string} : Specifies which group types to filter the results to. Valid values are:
|
||||||
* `Album`
|
* `Album`
|
||||||
* `All` // default
|
* `All` // default
|
||||||
|
@ -165,9 +165,13 @@ Returns a Promise with photo identifier objects from the local camera roll of th
|
||||||
* `All`
|
* `All`
|
||||||
* `Videos`
|
* `Videos`
|
||||||
* `Photos` // default
|
* `Photos` // default
|
||||||
* `mimeTypes` : {Array} : Filter by mimetype (e.g. image/jpeg).
|
* `mimeTypes` : {Array} : Filter by mimetype (e.g. image/jpeg). Note that using this will reduce performance slightly on iOS.
|
||||||
* `fromTime` : {timestamp} : Filter from date added.
|
* `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` : {timestamp} : Filter to date added.
|
* `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:
|
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}
|
* `group_name`: {string}
|
||||||
* `image`: {object} : An object with the following shape:
|
* `image`: {object} : An object with the following shape:
|
||||||
* `uri`: {string}
|
* `uri`: {string}
|
||||||
* `filename`: {string}
|
* `filename`: {string | null} : Only set if the `include` parameter contains `filename`.
|
||||||
* `height`: {number}
|
* `height`: {number}
|
||||||
* `width`: {number}
|
* `width`: {number}
|
||||||
* `fileSize`: {number}
|
* `fileSize`: {number | null} : Only set if the `include` parameter contains `fileSize`.
|
||||||
* `isStored`: {boolean}
|
* `isStored`: {boolean}
|
||||||
* `playableDuration`: {number}
|
* `playableDuration`: {number}
|
||||||
* `timestamp`: {number}
|
* `timestamp`: {number} : Timestamp in seconds.
|
||||||
* `location`: {object} : An object with the following shape:
|
* `location`: {object | null} : Only set if the `include` parameter contains `location`. An object with the following shape:
|
||||||
* `latitude`: {number}
|
* `latitude`: {number}
|
||||||
* `longitude`: {number}
|
* `longitude`: {number}
|
||||||
* `altitude`: {number}
|
* `altitude`: {number}
|
||||||
|
@ -234,7 +238,9 @@ render() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### `deletePhotos()`
|
### `deletePhotos()`
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
|
|
@ -47,9 +47,11 @@ import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
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_VIDEOS = "Videos";
|
||||||
private static final String ASSET_TYPE_ALL = "All";
|
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 = {
|
private static final String[] PROJECTION = {
|
||||||
Images.Media._ID,
|
Images.Media._ID,
|
||||||
Images.Media.MIME_TYPE,
|
Images.Media.MIME_TYPE,
|
||||||
|
@ -247,6 +253,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||||
ReadableArray mimeTypes = params.hasKey("mimeTypes")
|
ReadableArray mimeTypes = params.hasKey("mimeTypes")
|
||||||
? params.getArray("mimeTypes")
|
? params.getArray("mimeTypes")
|
||||||
: null;
|
: null;
|
||||||
|
ReadableArray include = params.hasKey("include") ? params.getArray("include") : null;
|
||||||
|
|
||||||
new GetMediaTask(
|
new GetMediaTask(
|
||||||
getReactApplicationContext(),
|
getReactApplicationContext(),
|
||||||
|
@ -257,6 +264,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||||
assetType,
|
assetType,
|
||||||
fromTime,
|
fromTime,
|
||||||
toTime,
|
toTime,
|
||||||
|
include,
|
||||||
promise)
|
promise)
|
||||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
}
|
}
|
||||||
|
@ -271,6 +279,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||||
private final String mAssetType;
|
private final String mAssetType;
|
||||||
private final long mFromTime;
|
private final long mFromTime;
|
||||||
private final long mToTime;
|
private final long mToTime;
|
||||||
|
private final Set<String> mInclude;
|
||||||
|
|
||||||
private GetMediaTask(
|
private GetMediaTask(
|
||||||
ReactContext context,
|
ReactContext context,
|
||||||
|
@ -281,6 +290,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||||
String assetType,
|
String assetType,
|
||||||
long fromTime,
|
long fromTime,
|
||||||
long toTime,
|
long toTime,
|
||||||
|
@Nullable ReadableArray include,
|
||||||
Promise promise) {
|
Promise promise) {
|
||||||
super(context);
|
super(context);
|
||||||
mContext = context;
|
mContext = context;
|
||||||
|
@ -292,6 +302,24 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||||
mAssetType = assetType;
|
mAssetType = assetType;
|
||||||
mFromTime = fromTime;
|
mFromTime = fromTime;
|
||||||
mToTime = toTime;
|
mToTime = toTime;
|
||||||
|
mInclude = createSetFromIncludeArray(include);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> createSetFromIncludeArray(@Nullable ReadableArray includeArray) {
|
||||||
|
Set<String> 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
|
@Override
|
||||||
|
@ -362,7 +390,7 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||||
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get media");
|
mPromise.reject(ERROR_UNABLE_TO_LOAD, "Could not get media");
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
putEdges(resolver, media, response, mFirst);
|
putEdges(resolver, media, response, mFirst, mInclude);
|
||||||
putPageInfo(media, response, mFirst, !TextUtils.isEmpty(mAfter) ? Integer.parseInt(mAfter) : 0);
|
putPageInfo(media, response, mFirst, !TextUtils.isEmpty(mAfter) ? Integer.parseInt(mAfter) : 0);
|
||||||
} finally {
|
} finally {
|
||||||
media.close();
|
media.close();
|
||||||
|
@ -463,10 +491,10 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||||
ContentResolver resolver,
|
ContentResolver resolver,
|
||||||
Cursor media,
|
Cursor media,
|
||||||
WritableMap response,
|
WritableMap response,
|
||||||
int limit) {
|
int limit,
|
||||||
|
Set<String> include) {
|
||||||
WritableArray edges = new WritableNativeArray();
|
WritableArray edges = new WritableNativeArray();
|
||||||
media.moveToFirst();
|
media.moveToFirst();
|
||||||
int idIndex = media.getColumnIndex(Images.Media._ID);
|
|
||||||
int mimeTypeIndex = media.getColumnIndex(Images.Media.MIME_TYPE);
|
int mimeTypeIndex = media.getColumnIndex(Images.Media.MIME_TYPE);
|
||||||
int groupNameIndex = media.getColumnIndex(Images.Media.BUCKET_DISPLAY_NAME);
|
int groupNameIndex = media.getColumnIndex(Images.Media.BUCKET_DISPLAY_NAME);
|
||||||
int dateTakenIndex = media.getColumnIndex(Images.Media.DATE_TAKEN);
|
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 sizeIndex = media.getColumnIndex(MediaStore.MediaColumns.SIZE);
|
||||||
int dataIndex = media.getColumnIndex(MediaStore.MediaColumns.DATA);
|
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++) {
|
for (int i = 0; i < limit && !media.isAfterLast(); i++) {
|
||||||
WritableMap edge = new WritableNativeMap();
|
WritableMap edge = new WritableNativeMap();
|
||||||
WritableMap node = new WritableNativeMap();
|
WritableMap node = new WritableNativeMap();
|
||||||
boolean imageInfoSuccess =
|
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) {
|
if (imageInfoSuccess) {
|
||||||
putBasicNodeInfo(media, node, mimeTypeIndex, groupNameIndex, dateTakenIndex);
|
putBasicNodeInfo(media, node, mimeTypeIndex, groupNameIndex, dateTakenIndex);
|
||||||
putLocationInfo(media, node, dataIndex);
|
putLocationInfo(media, node, dataIndex, includeLocation);
|
||||||
|
|
||||||
edge.putMap("node", node);
|
edge.putMap("node", node);
|
||||||
edges.pushMap(edge);
|
edges.pushMap(edge);
|
||||||
|
@ -511,22 +544,18 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||||
ContentResolver resolver,
|
ContentResolver resolver,
|
||||||
Cursor media,
|
Cursor media,
|
||||||
WritableMap node,
|
WritableMap node,
|
||||||
int idIndex,
|
|
||||||
int widthIndex,
|
int widthIndex,
|
||||||
int heightIndex,
|
int heightIndex,
|
||||||
int sizeIndex,
|
int sizeIndex,
|
||||||
int dataIndex,
|
int dataIndex,
|
||||||
int mimeTypeIndex) {
|
int mimeTypeIndex,
|
||||||
|
boolean includeFilename,
|
||||||
|
boolean includeFileSize) {
|
||||||
WritableMap image = new WritableNativeMap();
|
WritableMap image = new WritableNativeMap();
|
||||||
Uri photoUri = Uri.parse("file://" + media.getString(dataIndex));
|
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("uri", photoUri.toString());
|
||||||
image.putString("filename", strFileName);
|
|
||||||
float width = media.getInt(widthIndex);
|
float width = media.getInt(widthIndex);
|
||||||
float height = media.getInt(heightIndex);
|
float height = media.getInt(heightIndex);
|
||||||
long fileSize = media.getLong(sizeIndex);
|
|
||||||
|
|
||||||
String mimeType = media.getString(mimeTypeIndex);
|
String mimeType = media.getString(mimeTypeIndex);
|
||||||
|
|
||||||
if (mimeType != null
|
if (mimeType != null
|
||||||
|
@ -585,7 +614,21 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||||
}
|
}
|
||||||
image.putDouble("width", width);
|
image.putDouble("width", width);
|
||||||
image.putDouble("height", height);
|
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);
|
node.putMap("image", image);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -594,7 +637,13 @@ public class CameraRollModule extends ReactContextBaseJavaModule {
|
||||||
private static void putLocationInfo(
|
private static void putLocationInfo(
|
||||||
Cursor media,
|
Cursor media,
|
||||||
WritableMap node,
|
WritableMap node,
|
||||||
int dataIndex) {
|
int dataIndex,
|
||||||
|
boolean includeLocation) {
|
||||||
|
if (!includeLocation) {
|
||||||
|
node.putNull("location");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// location details are no longer indexed for privacy reasons using string Media.LATITUDE, Media.LONGITUDE
|
// location details are no longer indexed for privacy reasons using string Media.LATITUDE, Media.LONGITUDE
|
||||||
// we manually obtain location metadata using ExifInterface#getLatLong(float[]).
|
// we manually obtain location metadata using ExifInterface#getLatLong(float[]).
|
||||||
|
|
|
@ -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<Props> {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<CameraRollExample />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: '#F5FCFF',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -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 <ExampleContainer />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,6 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*
|
*
|
||||||
* @format
|
* @format
|
||||||
* @flow
|
|
||||||
*/
|
*/
|
||||||
import {AppRegistry} from 'react-native';
|
import {AppRegistry} from 'react-native';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
|
@ -57,7 +57,7 @@ export default class CameraRollExample extends React.Component<Props, State> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View style={styles.flex1}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Switch
|
<Switch
|
||||||
onValueChange={this._onSwitchChange}
|
onValueChange={this._onSwitchChange}
|
||||||
|
@ -107,7 +107,7 @@ export default class CameraRollExample extends React.Component<Props, State> {
|
||||||
onPress={this.loadAsset.bind(this, asset)}>
|
onPress={this.loadAsset.bind(this, asset)}>
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<Image source={{uri: asset.node.image.uri}} style={imageStyle} />
|
<Image source={{uri: asset.node.image.uri}} style={imageStyle} />
|
||||||
<View style={styles.info}>
|
<View style={styles.flex1}>
|
||||||
<Text style={styles.url}>{asset.node.image.uri}</Text>
|
<Text style={styles.url}>{asset.node.image.uri}</Text>
|
||||||
<Text>{locationStr}</Text>
|
<Text>{locationStr}</Text>
|
||||||
<Text>{asset.node.group_name}</Text>
|
<Text>{asset.node.group_name}</Text>
|
||||||
|
@ -135,7 +135,6 @@ export default class CameraRollExample extends React.Component<Props, State> {
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
header: {
|
header: {
|
||||||
marginTop: 44,
|
|
||||||
padding: 20,
|
padding: 20,
|
||||||
width: Dimensions.get('window').width,
|
width: Dimensions.get('window').width,
|
||||||
},
|
},
|
||||||
|
@ -150,7 +149,7 @@ const styles = StyleSheet.create({
|
||||||
image: {
|
image: {
|
||||||
margin: 4,
|
margin: 4,
|
||||||
},
|
},
|
||||||
info: {
|
flex1: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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<Props, State> {
|
||||||
|
state: State = {showChangeExampleModal: false, currentExampleIndex: 0};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {currentExampleIndex} = this.state;
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.flex1}>
|
||||||
|
<Button
|
||||||
|
title="Change example"
|
||||||
|
onPress={() => this.setState({showChangeExampleModal: true})}
|
||||||
|
/>
|
||||||
|
{this._renderChangeExampleModal()}
|
||||||
|
<View style={styles.flex1}>
|
||||||
|
{React.createElement(examples[currentExampleIndex].Component)}
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderChangeExampleModal() {
|
||||||
|
const {showChangeExampleModal} = this.state;
|
||||||
|
return (
|
||||||
|
<Modal visible={showChangeExampleModal} transparent>
|
||||||
|
<TouchableWithoutFeedback
|
||||||
|
onPress={() => this.setState({showChangeExampleModal: false})}>
|
||||||
|
<View style={styles.modalScrim}>
|
||||||
|
<SafeAreaView>
|
||||||
|
<View style={styles.modalInner}>
|
||||||
|
{examples.map((example, index) => (
|
||||||
|
<Button
|
||||||
|
key={example.label}
|
||||||
|
title={example.label}
|
||||||
|
onPress={() =>
|
||||||
|
this.setState({
|
||||||
|
currentExampleIndex: index,
|
||||||
|
showChangeExampleModal: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
modalScrim: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#00000080',
|
||||||
|
},
|
||||||
|
flex1: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
modalInner: {
|
||||||
|
margin: 20,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,175 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
Switch,
|
||||||
|
TextInput,
|
||||||
|
Keyboard,
|
||||||
|
} from 'react-native';
|
||||||
|
// @ts-ignore: CameraRollExample has no typings in same folder
|
||||||
|
import CameraRoll from '../../js/CameraRoll';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
fetchingPhotos: boolean;
|
||||||
|
timeTakenMillis: number | null;
|
||||||
|
output: CameraRoll.PhotoIdentifiersPage | null;
|
||||||
|
include: CameraRoll.Include[];
|
||||||
|
/**
|
||||||
|
* `first` argument passed into `getPhotos`, but as a string. Validate it
|
||||||
|
* with `this.first()` before using.
|
||||||
|
*/
|
||||||
|
firstStr: string;
|
||||||
|
/** `after` passed into `getPhotos`. Not passed if empty */
|
||||||
|
after: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const includeValues: CameraRoll.Include[] = [
|
||||||
|
'filename',
|
||||||
|
'fileSize',
|
||||||
|
'location',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example for testing performance differences between `getPhotos` and
|
||||||
|
* `getPhotosFast`
|
||||||
|
*/
|
||||||
|
export default class GetPhotosPerformanceExample extends React.PureComponent<
|
||||||
|
{},
|
||||||
|
State
|
||||||
|
> {
|
||||||
|
state: State = {
|
||||||
|
fetchingPhotos: false,
|
||||||
|
timeTakenMillis: null,
|
||||||
|
output: null,
|
||||||
|
include: [],
|
||||||
|
firstStr: '1000',
|
||||||
|
after: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
first = () => {
|
||||||
|
const first = parseInt(this.state.firstStr, 10);
|
||||||
|
if (first < 0 || !Number.isInteger(first)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return first;
|
||||||
|
};
|
||||||
|
|
||||||
|
startFetchingPhotos = async () => {
|
||||||
|
const {include} = this.state;
|
||||||
|
const first = this.first();
|
||||||
|
if (first === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({fetchingPhotos: true});
|
||||||
|
Keyboard.dismiss();
|
||||||
|
const params: CameraRoll.GetPhotosParams = {first, include};
|
||||||
|
const startTime = Date.now();
|
||||||
|
const output: CameraRoll.PhotoIdentifiersPage = await CameraRoll.getPhotos(
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
const endTime = Date.now();
|
||||||
|
this.setState({
|
||||||
|
output,
|
||||||
|
timeTakenMillis: endTime - startTime,
|
||||||
|
fetchingPhotos: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleIncludeChange = (
|
||||||
|
includeValue: CameraRoll.Include,
|
||||||
|
changedTo: boolean,
|
||||||
|
) => {
|
||||||
|
if (changedTo === false) {
|
||||||
|
const include = this.state.include.filter(
|
||||||
|
value => value !== includeValue,
|
||||||
|
);
|
||||||
|
this.setState({include});
|
||||||
|
} else {
|
||||||
|
const include = [...this.state.include, includeValue];
|
||||||
|
this.setState({include});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
fetchingPhotos,
|
||||||
|
timeTakenMillis,
|
||||||
|
output,
|
||||||
|
include,
|
||||||
|
firstStr,
|
||||||
|
} = this.state;
|
||||||
|
const first = this.first();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{includeValues.map(includeValue => (
|
||||||
|
<View key={includeValue} style={styles.inputRow}>
|
||||||
|
<Text>{includeValue}</Text>
|
||||||
|
<Switch
|
||||||
|
value={include.includes(includeValue)}
|
||||||
|
onValueChange={(changedTo: boolean) =>
|
||||||
|
this.handleIncludeChange(includeValue, changedTo)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
<View style={styles.inputRow}>
|
||||||
|
<Text>
|
||||||
|
first
|
||||||
|
{first === null && (
|
||||||
|
<Text style={styles.error}> (enter a positive number)</Text>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
value={firstStr}
|
||||||
|
onChangeText={(text: string) => this.setState({firstStr: text})}
|
||||||
|
style={[styles.textInput, first === null && styles.textInputError]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Button
|
||||||
|
disabled={fetchingPhotos}
|
||||||
|
title={`Run getPhotos on ${first} photos`}
|
||||||
|
onPress={this.startFetchingPhotos}
|
||||||
|
/>
|
||||||
|
{timeTakenMillis !== null && (
|
||||||
|
<Text>Time taken: {timeTakenMillis} ms</Text>
|
||||||
|
)}
|
||||||
|
<View>
|
||||||
|
<Text>Output</Text>
|
||||||
|
</View>
|
||||||
|
<TextInput
|
||||||
|
value={JSON.stringify(output, null, 2)}
|
||||||
|
multiline
|
||||||
|
style={styles.outputBox}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {flex: 1, padding: 8},
|
||||||
|
inputRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingVertical: 2,
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
borderColor: '#ccc',
|
||||||
|
borderWidth: 1,
|
||||||
|
paddingVertical: 4,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
error: {color: '#f00'},
|
||||||
|
textInputError: {borderColor: '#f00'},
|
||||||
|
outputBox: {
|
||||||
|
flex: 1,
|
||||||
|
borderColor: '#ccc',
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
});
|
|
@ -70,7 +70,7 @@ RCT_ENUM_CONVERTER(PHAssetCollectionSubtype, (@{
|
||||||
}
|
}
|
||||||
if (toTime > 0) {
|
if (toTime > 0) {
|
||||||
NSDate* toDate = [NSDate dateWithTimeIntervalSince1970:toTime/1000];
|
NSDate* toDate = [NSDate dateWithTimeIntervalSince1970:toTime/1000];
|
||||||
[format addObject:@"creationDate < %@"];
|
[format addObject:@"creationDate <= %@"];
|
||||||
[arguments addObject:toDate];
|
[arguments addObject:toDate];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,6 +255,11 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
||||||
NSUInteger const fromTime = [RCTConvert NSInteger:params[@"fromTime"]];
|
NSUInteger const fromTime = [RCTConvert NSInteger:params[@"fromTime"]];
|
||||||
NSUInteger const toTime = [RCTConvert NSInteger:params[@"toTime"]];
|
NSUInteger const toTime = [RCTConvert NSInteger:params[@"toTime"]];
|
||||||
NSArray<NSString *> *const mimeTypes = [RCTConvert NSStringArray:params[@"mimeTypes"]];
|
NSArray<NSString *> *const mimeTypes = [RCTConvert NSStringArray:params[@"mimeTypes"]];
|
||||||
|
NSArray<NSString *> *const include = [RCTConvert NSStringArray:params[@"include"]];
|
||||||
|
|
||||||
|
BOOL __block includeFilename = [include indexOfObject:@"filename"] != NSNotFound;
|
||||||
|
BOOL __block includeFileSize = [include indexOfObject:@"fileSize"] != NSNotFound;
|
||||||
|
BOOL __block includeLocation = [include indexOfObject:@"location"] != NSNotFound;
|
||||||
|
|
||||||
// If groupTypes is "all", we want to fetch the SmartAlbum "all photos". Otherwise, all
|
// If groupTypes is "all", we want to fetch the SmartAlbum "all photos". Otherwise, all
|
||||||
// other groupTypes values require the "album" collection type.
|
// other groupTypes values require the "album" collection type.
|
||||||
|
@ -265,6 +270,18 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
||||||
|
|
||||||
// Predicate for fetching assets within a collection
|
// Predicate for fetching assets within a collection
|
||||||
PHFetchOptions *const assetFetchOptions = [RCTConvert PHFetchOptionsFromMediaType:mediaType fromTime:fromTime toTime:toTime];
|
PHFetchOptions *const assetFetchOptions = [RCTConvert PHFetchOptionsFromMediaType:mediaType fromTime:fromTime toTime:toTime];
|
||||||
|
// We can directly set the limit if we guarantee every image fetched will be
|
||||||
|
// added to the output array within the `collectAsset` block
|
||||||
|
BOOL collectAssetMayOmitAsset = !!afterCursor || [mimeTypes count] > 0;
|
||||||
|
if (!collectAssetMayOmitAsset) {
|
||||||
|
// We set the fetchLimit to first + 1 so that `hasNextPage` will be set
|
||||||
|
// correctly:
|
||||||
|
// - If the user set `first: 10` and there are 11 photos, `hasNextPage`
|
||||||
|
// will be set to true below inside of `collectAsset`
|
||||||
|
// - If the user set `first: 10` and there are 10 photos, `hasNextPage`
|
||||||
|
// will not be set, as expected
|
||||||
|
assetFetchOptions.fetchLimit = first + 1;
|
||||||
|
}
|
||||||
assetFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
|
assetFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
|
||||||
|
|
||||||
BOOL __block foundAfter = NO;
|
BOOL __block foundAfter = NO;
|
||||||
|
@ -277,7 +294,6 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
||||||
collectionFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"endDate" ascending:NO]];
|
collectionFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"endDate" ascending:NO]];
|
||||||
if (groupName != nil) {
|
if (groupName != nil) {
|
||||||
collectionFetchOptions.predicate = [NSPredicate predicateWithFormat:@"localizedTitle = %@", groupName];
|
collectionFetchOptions.predicate = [NSPredicate predicateWithFormat:@"localizedTitle = %@", groupName];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BOOL __block stopCollections_;
|
BOOL __block stopCollections_;
|
||||||
|
@ -286,6 +302,24 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
||||||
requestPhotoLibraryAccess(reject, ^{
|
requestPhotoLibraryAccess(reject, ^{
|
||||||
void (^collectAsset)(PHAsset*, NSUInteger, BOOL*) = ^(PHAsset * _Nonnull asset, NSUInteger assetIdx, BOOL * _Nonnull stopAssets) {
|
void (^collectAsset)(PHAsset*, NSUInteger, BOOL*) = ^(PHAsset * _Nonnull asset, NSUInteger assetIdx, BOOL * _Nonnull stopAssets) {
|
||||||
NSString *const uri = [NSString stringWithFormat:@"ph://%@", [asset localIdentifier]];
|
NSString *const uri = [NSString stringWithFormat:@"ph://%@", [asset localIdentifier]];
|
||||||
|
NSString *_Nullable originalFilename = NULL;
|
||||||
|
PHAssetResource *_Nullable resource = NULL;
|
||||||
|
NSNumber* fileSize = [NSNumber numberWithInt:0];
|
||||||
|
|
||||||
|
if (includeFilename || includeFileSize || [mimeTypes count] > 0) {
|
||||||
|
// Get underlying resources of an asset - this includes files as well as details about edited PHAssets
|
||||||
|
// This is required for the filename and mimeType filtering
|
||||||
|
NSArray<PHAssetResource *> *const assetResources = [PHAssetResource assetResourcesForAsset:asset];
|
||||||
|
resource = [assetResources firstObject];
|
||||||
|
originalFilename = resource.originalFilename;
|
||||||
|
fileSize = [resource valueForKey:@"fileSize"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// WARNING: If you add any code to `collectAsset` that may skip adding an
|
||||||
|
// asset to the `assets` output array, you should do it inside this
|
||||||
|
// block and ensure the logic for `collectAssetMayOmitAsset` above is
|
||||||
|
// updated
|
||||||
|
if (collectAssetMayOmitAsset) {
|
||||||
if (afterCursor && !foundAfter) {
|
if (afterCursor && !foundAfter) {
|
||||||
if ([afterCursor isEqualToString:uri]) {
|
if ([afterCursor isEqualToString:uri]) {
|
||||||
foundAfter = YES;
|
foundAfter = YES;
|
||||||
|
@ -293,14 +327,8 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
||||||
return; // skip until we get to the first one
|
return; // skip until we get to the first one
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get underlying resources of an asset - this includes files as well as details about edited PHAssets
|
|
||||||
NSArray<PHAssetResource *> *const assetResources = [PHAssetResource assetResourcesForAsset:asset];
|
|
||||||
if (![assetResources firstObject]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
PHAssetResource *const _Nonnull resource = [assetResources firstObject];
|
|
||||||
|
|
||||||
if ([mimeTypes count] > 0) {
|
if ([mimeTypes count] > 0 && resource) {
|
||||||
CFStringRef const uti = (__bridge CFStringRef _Nonnull)(resource.uniformTypeIdentifier);
|
CFStringRef const uti = (__bridge CFStringRef _Nonnull)(resource.uniformTypeIdentifier);
|
||||||
NSString *const mimeType = (NSString *)CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType));
|
NSString *const mimeType = (NSString *)CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType));
|
||||||
|
|
||||||
|
@ -316,6 +344,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If we've accumulated enough results to resolve a single promise
|
// If we've accumulated enough results to resolve a single promise
|
||||||
if (first == assets.count) {
|
if (first == assets.count) {
|
||||||
|
@ -336,7 +365,6 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
||||||
? @"audio"
|
? @"audio"
|
||||||
: @"unknown")));
|
: @"unknown")));
|
||||||
CLLocation *const loc = asset.location;
|
CLLocation *const loc = asset.location;
|
||||||
NSString *const origFilename = resource.originalFilename;
|
|
||||||
|
|
||||||
// A note on isStored: in the previous code that used ALAssets, isStored
|
// A note on isStored: in the previous code that used ALAssets, isStored
|
||||||
// was always set to YES, probably because iCloud-synced images were never returned (?).
|
// was always set to YES, probably because iCloud-synced images were never returned (?).
|
||||||
|
@ -351,21 +379,21 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
||||||
@"group_name": currentCollectionName,
|
@"group_name": currentCollectionName,
|
||||||
@"image": @{
|
@"image": @{
|
||||||
@"uri": uri,
|
@"uri": uri,
|
||||||
@"filename": origFilename,
|
@"filename": (includeFilename && originalFilename ? originalFilename : [NSNull null]),
|
||||||
@"height": @([asset pixelHeight]),
|
@"height": @([asset pixelHeight]),
|
||||||
@"width": @([asset pixelWidth]),
|
@"width": @([asset pixelWidth]),
|
||||||
@"fileSize": [resource valueForKey:@"fileSize"],
|
@"fileSize": (includeFileSize ? fileSize : [NSNull null]),
|
||||||
@"isStored": @YES, // this field doesn't seem to exist on android
|
@"isStored": @YES, // this field doesn't seem to exist on android
|
||||||
@"playableDuration": @([asset duration]) // fractional seconds
|
@"playableDuration": @([asset duration]) // fractional seconds
|
||||||
},
|
},
|
||||||
@"timestamp": @(asset.creationDate.timeIntervalSince1970),
|
@"timestamp": @(asset.creationDate.timeIntervalSince1970),
|
||||||
@"location": (loc ? @{
|
@"location": (includeLocation && loc ? @{
|
||||||
@"latitude": @(loc.coordinate.latitude),
|
@"latitude": @(loc.coordinate.latitude),
|
||||||
@"longitude": @(loc.coordinate.longitude),
|
@"longitude": @(loc.coordinate.longitude),
|
||||||
@"altitude": @(loc.altitude),
|
@"altitude": @(loc.altitude),
|
||||||
@"heading": @(loc.course),
|
@"heading": @(loc.course),
|
||||||
@"speed": @(loc.speed), // speed in m/s
|
@"speed": @(loc.speed), // speed in m/s
|
||||||
} : @{})
|
} : [NSNull null])
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,6 +31,8 @@ const ASSET_TYPE_OPTIONS = {
|
||||||
|
|
||||||
export type GroupTypes = $Keys<typeof GROUP_TYPES_OPTIONS>;
|
export type GroupTypes = $Keys<typeof GROUP_TYPES_OPTIONS>;
|
||||||
|
|
||||||
|
export type Include = 'filename' | 'fileSize' | 'location';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shape of the param arg for the `getPhotos` function.
|
* Shape of the param arg for the `getPhotos` function.
|
||||||
*/
|
*/
|
||||||
|
@ -63,10 +65,26 @@ export type GetPhotosParams = {
|
||||||
*/
|
*/
|
||||||
assetType?: $Keys<typeof ASSET_TYPE_OPTIONS>,
|
assetType?: $Keys<typeof ASSET_TYPE_OPTIONS>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Earliest time to get photos from. A timestamp in milliseconds. Exclusive.
|
||||||
|
*/
|
||||||
|
fromTime?: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Latest time to get photos from. A timestamp in milliseconds. Inclusive.
|
||||||
|
*/
|
||||||
|
toTime?: Number,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter by mimetype (e.g. image/jpeg).
|
* Filter by mimetype (e.g. image/jpeg).
|
||||||
*/
|
*/
|
||||||
mimeTypes?: Array<string>,
|
mimeTypes?: Array<string>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specific fields in the output that we want to include, even though they
|
||||||
|
* might have some performance impact.
|
||||||
|
*/
|
||||||
|
include?: Include[],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PhotoIdentifier = {
|
export type PhotoIdentifier = {
|
||||||
|
@ -74,22 +92,22 @@ export type PhotoIdentifier = {
|
||||||
type: string,
|
type: string,
|
||||||
group_name: string,
|
group_name: string,
|
||||||
image: {
|
image: {
|
||||||
filename: string,
|
filename: string | null,
|
||||||
uri: string,
|
uri: string,
|
||||||
height: number,
|
height: number,
|
||||||
width: number,
|
width: number,
|
||||||
fileSize: number,
|
fileSize: number | null,
|
||||||
isStored?: boolean,
|
isStored?: boolean,
|
||||||
playableDuration: number,
|
playableDuration: number,
|
||||||
},
|
},
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
location?: {
|
location: {
|
||||||
latitude?: number,
|
latitude?: number,
|
||||||
longitude?: number,
|
longitude?: number,
|
||||||
altitude?: number,
|
altitude?: number,
|
||||||
heading?: number,
|
heading?: number,
|
||||||
speed?: number,
|
speed?: number,
|
||||||
},
|
} | null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -114,6 +132,7 @@ export type Album = {
|
||||||
title: string,
|
title: string,
|
||||||
count: number,
|
count: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `CameraRoll` provides access to the local camera roll or photo library.
|
* `CameraRoll` provides access to the local camera roll or photo library.
|
||||||
*
|
*
|
||||||
|
@ -186,6 +205,18 @@ class CameraRoll {
|
||||||
): Promise<Album[]> {
|
): Promise<Album[]> {
|
||||||
return RNCCameraRoll.getAlbums(params);
|
return RNCCameraRoll.getAlbums(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getParamsWithDefaults(params: GetPhotosParams): GetPhotosParams {
|
||||||
|
const newParams = {...params};
|
||||||
|
if (!newParams.assetType) {
|
||||||
|
newParams.assetType = ASSET_TYPE_OPTIONS.All;
|
||||||
|
}
|
||||||
|
if (!newParams.groupTypes && Platform.OS !== 'android') {
|
||||||
|
newParams.groupTypes = GROUP_TYPES_OPTIONS.All;
|
||||||
|
}
|
||||||
|
return newParams;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a Promise with photo identifier objects from the local camera
|
* Returns a Promise with photo identifier objects from the local camera
|
||||||
* roll of the device matching shape defined by `getPhotosReturnChecker`.
|
* roll of the device matching shape defined by `getPhotosReturnChecker`.
|
||||||
|
@ -193,21 +224,19 @@ class CameraRoll {
|
||||||
* See https://facebook.github.io/react-native/docs/cameraroll.html#getphotos
|
* See https://facebook.github.io/react-native/docs/cameraroll.html#getphotos
|
||||||
*/
|
*/
|
||||||
static getPhotos(params: GetPhotosParams): Promise<PhotoIdentifiersPage> {
|
static getPhotos(params: GetPhotosParams): Promise<PhotoIdentifiersPage> {
|
||||||
if (!params.assetType) {
|
params = CameraRoll.getParamsWithDefaults(params);
|
||||||
params.assetType = ASSET_TYPE_OPTIONS.All;
|
const promise = RNCCameraRoll.getPhotos(params);
|
||||||
}
|
|
||||||
if (!params.groupTypes && Platform.OS !== 'android') {
|
|
||||||
params.groupTypes = GROUP_TYPES_OPTIONS.All;
|
|
||||||
}
|
|
||||||
if (arguments.length > 1) {
|
if (arguments.length > 1) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'CameraRoll.getPhotos(tag, success, error) is deprecated. Use the returned Promise instead',
|
'CameraRoll.getPhotos(tag, success, error) is deprecated. Use the returned Promise instead',
|
||||||
);
|
);
|
||||||
let successCallback = arguments[1];
|
let successCallback = arguments[1];
|
||||||
const errorCallback = arguments[2] || (() => {});
|
const errorCallback = arguments[2] || (() => {});
|
||||||
RNCCameraRoll.getPhotos(params).then(successCallback, errorCallback);
|
promise.then(successCallback, errorCallback);
|
||||||
}
|
}
|
||||||
return RNCCameraRoll.getPhotos(params);
|
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
"@babel/runtime": "^7.9.6",
|
"@babel/runtime": "^7.9.6",
|
||||||
"@react-native-community/eslint-config": "^1.1.0",
|
"@react-native-community/eslint-config": "^1.1.0",
|
||||||
"@semantic-release/git": "7.0.8",
|
"@semantic-release/git": "7.0.8",
|
||||||
|
"@types/react-native": "^0.62.10",
|
||||||
"babel-core": "^7.0.0-bridge.0",
|
"babel-core": "^7.0.0-bridge.0",
|
||||||
"babel-jest": "^26.0.1",
|
"babel-jest": "^26.0.1",
|
||||||
"babel-plugin-module-resolver": "^3.2.0",
|
"babel-plugin-module-resolver": "^3.2.0",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"include": ["typings/**/*.d.ts"],
|
"include": ["typings/**/*.d.ts", "example/**/*.ts", "example/**/*.tsx"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
|
@ -16,10 +16,10 @@
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"lib": ["es2015", "es2016", "esnext", "dom"]
|
"lib": ["es2015", "es2016", "esnext", "dom"],
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||||
"node_modules",
|
|
||||||
"**/*.spec.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*
|
|
||||||
* @format
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare namespace CameraRoll {
|
declare namespace CameraRoll {
|
||||||
|
@ -19,47 +17,107 @@ declare namespace CameraRoll {
|
||||||
|
|
||||||
type AssetType = 'All' | 'Videos' | 'Photos';
|
type AssetType = 'All' | 'Videos' | 'Photos';
|
||||||
|
|
||||||
|
type Include =
|
||||||
|
/** Ensures the filename is included. Has a large performance hit on iOS */
|
||||||
|
| 'filename'
|
||||||
|
/** Ensures the fileSize is included. Has a large performance hit on iOS */
|
||||||
|
| 'fileSize'
|
||||||
|
/** Ensures the location is included. Has a medium performance hit on Android */
|
||||||
|
| 'location';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shape of the param arg for the `getPhotosFast` function.
|
||||||
|
*/
|
||||||
interface GetPhotosParams {
|
interface GetPhotosParams {
|
||||||
|
/**
|
||||||
|
* The number of photos wanted in reverse order of the photo application
|
||||||
|
* (i.e. most recent first).
|
||||||
|
*/
|
||||||
first: number;
|
first: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
after?: string;
|
after?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies which group types to filter the results to.
|
||||||
|
*/
|
||||||
groupTypes?: GroupType;
|
groupTypes?: GroupType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies filter on group names, like 'Recent Photos' or custom album
|
||||||
|
* titles.
|
||||||
|
*/
|
||||||
groupName?: string;
|
groupName?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies filter on asset type
|
||||||
|
*/
|
||||||
assetType?: AssetType;
|
assetType?: AssetType;
|
||||||
mimeTypes?: Array<string>;
|
|
||||||
|
/**
|
||||||
|
* Filter by creation time with a timestamp in milliseconds. This time is
|
||||||
|
* exclusive, so we'll select all photos with `timestamp > fromTime`.
|
||||||
|
*/
|
||||||
fromTime?: number;
|
fromTime?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by creation time with a timestamp in milliseconds. This time is
|
||||||
|
* inclusive, so we'll select all photos with `timestamp <= toTime`.
|
||||||
|
*/
|
||||||
toTime?: number;
|
toTime?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by mimetype (e.g. image/jpeg). Note that using this will reduce
|
||||||
|
* performance slightly on iOS.
|
||||||
|
*/
|
||||||
|
mimeTypes?: Array<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specific fields in the output that we want to include, even though they
|
||||||
|
* might have some performance impact.
|
||||||
|
*/
|
||||||
|
include?: Include[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PhotoIdentifier {
|
interface PhotoIdentifier {
|
||||||
node: {
|
node: {
|
||||||
type: string,
|
type: string;
|
||||||
group_name: string,
|
group_name: string;
|
||||||
image: {
|
image: {
|
||||||
filename: string,
|
/** Only set if the `include` parameter contains `filename`. */
|
||||||
uri: string,
|
filename: string | null;
|
||||||
height: number,
|
uri: string;
|
||||||
width: number,
|
height: number;
|
||||||
fileSize: number,
|
width: number;
|
||||||
isStored?: boolean,
|
/** Only set if the `include` parameter contains `fileSize`. */
|
||||||
playableDuration: number,
|
fileSize: number | null;
|
||||||
},
|
isStored?: boolean;
|
||||||
timestamp: number,
|
playableDuration: number;
|
||||||
location?: {
|
};
|
||||||
latitude?: number,
|
/** Timestamp in seconds. */
|
||||||
longitude?: number,
|
timestamp: number;
|
||||||
altitude?: number,
|
/** Only set if the `include` parameter contains `location`. */
|
||||||
heading?: number,
|
location: {
|
||||||
speed?: number,
|
latitude?: number;
|
||||||
},
|
longitude?: number;
|
||||||
|
altitude?: number;
|
||||||
|
heading?: number;
|
||||||
|
speed?: number;
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PhotoIdentifiersPage {
|
interface PhotoIdentifiersPage {
|
||||||
edges: Array<PhotoIdentifier>;
|
edges: Array<PhotoIdentifier>;
|
||||||
page_info: {
|
page_info: {
|
||||||
has_next_page: boolean,
|
has_next_page: boolean;
|
||||||
start_cursor?: string,
|
start_cursor?: string;
|
||||||
end_cursor?: string,
|
end_cursor?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,8 +131,8 @@ declare namespace CameraRoll {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SaveToCameraRollOptions = {
|
type SaveToCameraRollOptions = {
|
||||||
type?: 'photo' | 'video' | 'auto',
|
type?: 'photo' | 'video' | 'auto';
|
||||||
album?: string,
|
album?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -90,12 +148,18 @@ declare namespace CameraRoll {
|
||||||
/**
|
/**
|
||||||
* Saves the photo or video to the camera roll or photo library.
|
* Saves the photo or video to the camera roll or photo library.
|
||||||
*/
|
*/
|
||||||
function saveToCameraRoll(tag: string, type?: 'photo' | 'video'): Promise<string>;
|
function saveToCameraRoll(
|
||||||
|
tag: string,
|
||||||
|
type?: 'photo' | 'video',
|
||||||
|
): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the photo or video to the camera roll or photo library.
|
* Saves the photo or video to the camera roll or photo library.
|
||||||
*/
|
*/
|
||||||
function save(tag: string, options?: SaveToCameraRollOptions): Promise<string>
|
function save(
|
||||||
|
tag: string,
|
||||||
|
options?: SaveToCameraRollOptions,
|
||||||
|
): Promise<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a Promise with photo identifier objects from the local camera
|
* Returns a Promise with photo identifier objects from the local camera
|
||||||
|
|
25
yarn.lock
25
yarn.lock
|
@ -1598,6 +1598,26 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.0.tgz#dc85454b953178cc6043df5208b9e949b54a3bc4"
|
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.0.tgz#dc85454b953178cc6043df5208b9e949b54a3bc4"
|
||||||
integrity sha512-/rM+sWiuOZ5dvuVzV37sUuklsbg+JPOP8d+nNFlo2ZtfpzPiPvh1/gc8liWOLBqe+sR+ZM7guPaIcTt6UZTo7Q==
|
integrity sha512-/rM+sWiuOZ5dvuVzV37sUuklsbg+JPOP8d+nNFlo2ZtfpzPiPvh1/gc8liWOLBqe+sR+ZM7guPaIcTt6UZTo7Q==
|
||||||
|
|
||||||
|
"@types/prop-types@*":
|
||||||
|
version "15.7.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
|
||||||
|
integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
|
||||||
|
|
||||||
|
"@types/react-native@^0.62.10":
|
||||||
|
version "0.62.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.62.10.tgz#82c481df21db4e7460755dc3fc7091e333a1d2bd"
|
||||||
|
integrity sha512-QR4PGrzZ3IvRIHlScyIPuv2GV8tD/BMICZz514KGvn3KHbh0mLphHHemtHZC1o8u4xM5LxwVpMpMYHQ+ncSfag==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react@*":
|
||||||
|
version "16.9.35"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
|
||||||
|
integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
csstype "^2.2.0"
|
||||||
|
|
||||||
"@types/stack-utils@^1.0.1":
|
"@types/stack-utils@^1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
||||||
|
@ -3157,6 +3177,11 @@ cssstyle@^2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
cssom "~0.3.6"
|
cssom "~0.3.6"
|
||||||
|
|
||||||
|
csstype@^2.2.0:
|
||||||
|
version "2.6.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b"
|
||||||
|
integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==
|
||||||
|
|
||||||
currently-unhandled@^0.4.1:
|
currently-unhandled@^0.4.1:
|
||||||
version "0.4.1"
|
version "0.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
|
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
|
||||||
|
|
Loading…
Reference in New Issue