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,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": false,
|
||||
"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. |
|
||||
|
||||
* `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}
|
||||
|
@ -234,7 +238,9 @@ render() {
|
|||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `deletePhotos()`
|
||||
|
||||
```javascript
|
||||
|
|
|
@ -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<String> 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<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
|
||||
|
@ -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<String> 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[]).
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
import {AppRegistry} from 'react-native';
|
||||
import App from './App';
|
||||
|
|
|
@ -57,7 +57,7 @@ export default class CameraRollExample extends React.Component<Props, State> {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<View>
|
||||
<View style={styles.flex1}>
|
||||
<View style={styles.header}>
|
||||
<Switch
|
||||
onValueChange={this._onSwitchChange}
|
||||
|
@ -107,7 +107,7 @@ export default class CameraRollExample extends React.Component<Props, State> {
|
|||
onPress={this.loadAsset.bind(this, asset)}>
|
||||
<View style={styles.row}>
|
||||
<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>{locationStr}</Text>
|
||||
<Text>{asset.node.group_name}</Text>
|
||||
|
@ -135,7 +135,6 @@ export default class CameraRollExample extends React.Component<Props, State> {
|
|||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
NSDate* toDate = [NSDate dateWithTimeIntervalSince1970:toTime/1000];
|
||||
[format addObject:@"creationDate < %@"];
|
||||
[format addObject:@"creationDate <= %@"];
|
||||
[arguments addObject:toDate];
|
||||
}
|
||||
|
||||
|
@ -255,6 +255,11 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
|||
NSUInteger const fromTime = [RCTConvert NSInteger:params[@"fromTime"]];
|
||||
NSUInteger const toTime = [RCTConvert NSInteger:params[@"toTime"]];
|
||||
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
|
||||
// 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
|
||||
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]];
|
||||
|
||||
BOOL __block foundAfter = NO;
|
||||
|
@ -277,7 +294,6 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
|||
collectionFetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"endDate" ascending:NO]];
|
||||
if (groupName != nil) {
|
||||
collectionFetchOptions.predicate = [NSPredicate predicateWithFormat:@"localizedTitle = %@", groupName];
|
||||
|
||||
}
|
||||
|
||||
BOOL __block stopCollections_;
|
||||
|
@ -286,6 +302,24 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
|||
requestPhotoLibraryAccess(reject, ^{
|
||||
void (^collectAsset)(PHAsset*, NSUInteger, BOOL*) = ^(PHAsset * _Nonnull asset, NSUInteger assetIdx, BOOL * _Nonnull stopAssets) {
|
||||
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 isEqualToString:uri]) {
|
||||
foundAfter = YES;
|
||||
|
@ -293,14 +327,8 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
|||
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);
|
||||
NSString *const mimeType = (NSString *)CFBridgingRelease(UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType));
|
||||
|
||||
|
@ -316,6 +344,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
|||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we've accumulated enough results to resolve a single promise
|
||||
if (first == assets.count) {
|
||||
|
@ -336,7 +365,6 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
|||
? @"audio"
|
||||
: @"unknown")));
|
||||
CLLocation *const loc = asset.location;
|
||||
NSString *const origFilename = resource.originalFilename;
|
||||
|
||||
// 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 (?).
|
||||
|
@ -351,21 +379,21 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params
|
|||
@"group_name": currentCollectionName,
|
||||
@"image": @{
|
||||
@"uri": uri,
|
||||
@"filename": origFilename,
|
||||
@"filename": (includeFilename && originalFilename ? originalFilename : [NSNull null]),
|
||||
@"height": @([asset pixelHeight]),
|
||||
@"width": @([asset pixelWidth]),
|
||||
@"fileSize": [resource valueForKey:@"fileSize"],
|
||||
@"fileSize": (includeFileSize ? fileSize : [NSNull null]),
|
||||
@"isStored": @YES, // this field doesn't seem to exist on android
|
||||
@"playableDuration": @([asset duration]) // fractional seconds
|
||||
},
|
||||
@"timestamp": @(asset.creationDate.timeIntervalSince1970),
|
||||
@"location": (loc ? @{
|
||||
@"location": (includeLocation && loc ? @{
|
||||
@"latitude": @(loc.coordinate.latitude),
|
||||
@"longitude": @(loc.coordinate.longitude),
|
||||
@"altitude": @(loc.altitude),
|
||||
@"heading": @(loc.course),
|
||||
@"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 Include = 'filename' | 'fileSize' | 'location';
|
||||
|
||||
/**
|
||||
* Shape of the param arg for the `getPhotos` function.
|
||||
*/
|
||||
|
@ -63,10 +65,26 @@ export type GetPhotosParams = {
|
|||
*/
|
||||
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).
|
||||
*/
|
||||
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 = {
|
||||
|
@ -74,22 +92,22 @@ export type PhotoIdentifier = {
|
|||
type: string,
|
||||
group_name: string,
|
||||
image: {
|
||||
filename: string,
|
||||
filename: string | null,
|
||||
uri: string,
|
||||
height: number,
|
||||
width: number,
|
||||
fileSize: number,
|
||||
fileSize: number | null,
|
||||
isStored?: boolean,
|
||||
playableDuration: number,
|
||||
},
|
||||
timestamp: number,
|
||||
location?: {
|
||||
location: {
|
||||
latitude?: number,
|
||||
longitude?: number,
|
||||
altitude?: number,
|
||||
heading?: number,
|
||||
speed?: number,
|
||||
},
|
||||
} | null,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -114,6 +132,7 @@ export type Album = {
|
|||
title: string,
|
||||
count: number,
|
||||
};
|
||||
|
||||
/**
|
||||
* `CameraRoll` provides access to the local camera roll or photo library.
|
||||
*
|
||||
|
@ -186,6 +205,18 @@ class CameraRoll {
|
|||
): Promise<Album[]> {
|
||||
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
|
||||
* 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
|
||||
*/
|
||||
static getPhotos(params: GetPhotosParams): Promise<PhotoIdentifiersPage> {
|
||||
if (!params.assetType) {
|
||||
params.assetType = ASSET_TYPE_OPTIONS.All;
|
||||
}
|
||||
if (!params.groupTypes && Platform.OS !== 'android') {
|
||||
params.groupTypes = GROUP_TYPES_OPTIONS.All;
|
||||
}
|
||||
params = CameraRoll.getParamsWithDefaults(params);
|
||||
const promise = RNCCameraRoll.getPhotos(params);
|
||||
|
||||
if (arguments.length > 1) {
|
||||
console.warn(
|
||||
'CameraRoll.getPhotos(tag, success, error) is deprecated. Use the returned Promise instead',
|
||||
);
|
||||
let successCallback = arguments[1];
|
||||
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",
|
||||
"@react-native-community/eslint-config": "^1.1.0",
|
||||
"@semantic-release/git": "7.0.8",
|
||||
"@types/react-native": "^0.62.10",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
"babel-jest": "^26.0.1",
|
||||
"babel-plugin-module-resolver": "^3.2.0",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"include": ["typings/**/*.d.ts"],
|
||||
"include": ["typings/**/*.d.ts", "example/**/*.ts", "example/**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
|
@ -16,10 +16,10 @@
|
|||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"lib": ["es2015", "es2016", "esnext", "dom"]
|
||||
"lib": ["es2015", "es2016", "esnext", "dom"],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.spec.ts"
|
||||
]
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
declare namespace CameraRoll {
|
||||
|
@ -19,47 +17,107 @@ declare namespace CameraRoll {
|
|||
|
||||
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 {
|
||||
/**
|
||||
* The number of photos wanted in reverse order of the photo application
|
||||
* (i.e. most recent first).
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Specifies which group types to filter the results to.
|
||||
*/
|
||||
groupTypes?: GroupType;
|
||||
|
||||
/**
|
||||
* Specifies filter on group names, like 'Recent Photos' or custom album
|
||||
* titles.
|
||||
*/
|
||||
groupName?: string;
|
||||
|
||||
/**
|
||||
* Specifies filter on asset type
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Filter by creation time with a timestamp in milliseconds. This time is
|
||||
* inclusive, so we'll select all photos with `timestamp <= toTime`.
|
||||
*/
|
||||
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 {
|
||||
node: {
|
||||
type: string,
|
||||
group_name: string,
|
||||
type: string;
|
||||
group_name: string;
|
||||
image: {
|
||||
filename: string,
|
||||
uri: string,
|
||||
height: number,
|
||||
width: number,
|
||||
fileSize: number,
|
||||
isStored?: boolean,
|
||||
playableDuration: number,
|
||||
},
|
||||
timestamp: number,
|
||||
location?: {
|
||||
latitude?: number,
|
||||
longitude?: number,
|
||||
altitude?: number,
|
||||
heading?: number,
|
||||
speed?: number,
|
||||
},
|
||||
/** Only set if the `include` parameter contains `filename`. */
|
||||
filename: string | null;
|
||||
uri: string;
|
||||
height: number;
|
||||
width: number;
|
||||
/** Only set if the `include` parameter contains `fileSize`. */
|
||||
fileSize: number | null;
|
||||
isStored?: boolean;
|
||||
playableDuration: number;
|
||||
};
|
||||
/** Timestamp in seconds. */
|
||||
timestamp: number;
|
||||
/** Only set if the `include` parameter contains `location`. */
|
||||
location: {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
altitude?: number;
|
||||
heading?: number;
|
||||
speed?: number;
|
||||
} | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface PhotoIdentifiersPage {
|
||||
edges: Array<PhotoIdentifier>;
|
||||
page_info: {
|
||||
has_next_page: boolean,
|
||||
start_cursor?: string,
|
||||
end_cursor?: string,
|
||||
has_next_page: boolean;
|
||||
start_cursor?: string;
|
||||
end_cursor?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -73,8 +131,8 @@ declare namespace CameraRoll {
|
|||
}
|
||||
|
||||
type SaveToCameraRollOptions = {
|
||||
type?: 'photo' | 'video' | 'auto',
|
||||
album?: string,
|
||||
type?: 'photo' | 'video' | 'auto';
|
||||
album?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -90,12 +148,18 @@ declare namespace CameraRoll {
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
|
|
25
yarn.lock
25
yarn.lock
|
@ -1598,6 +1598,26 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.0.tgz#dc85454b953178cc6043df5208b9e949b54a3bc4"
|
||||
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":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
||||
|
@ -3157,6 +3177,11 @@ cssstyle@^2.2.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
|
||||
|
|
Loading…
Reference in New Issue