e2e configuration and example transferred
This commit is contained in:
parent
02cf845006
commit
9f176640d8
|
@ -10,6 +10,7 @@
|
|||
import React, {Component} from 'react';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
import CameraRoll from '@react-native-community/cameraroll';
|
||||
import CameraRollExample from './js/CameraRollExample';
|
||||
|
||||
type Props = {};
|
||||
export default class App extends Component<Props> {
|
||||
|
@ -20,7 +21,7 @@ export default class App extends Component<Props> {
|
|||
render() {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Welcome</Text>
|
||||
<CameraRollExample />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"setupFilesAfterEnv": ["./init.js"],
|
||||
"testEnvironment": "node"
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
/* eslint-env jest, jasmine */
|
||||
|
||||
const detox = require('detox');
|
||||
const config = require('../../package.json').detox;
|
||||
const adapter = require('detox/runners/jest/adapter');
|
||||
|
||||
jest.setTimeout(300000);
|
||||
jasmine.getEnv().addReporter(adapter);
|
||||
|
||||
beforeAll(async () => {
|
||||
await detox.init(config, {initGlobals: false, launchApp: false});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await adapter.beforeEach();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await adapter.afterAll();
|
||||
await detox.cleanup();
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
const {device, expect, element, by, waitFor} = require('detox');
|
||||
|
||||
describe('CameraRoll', () => {
|
||||
beforeEach(async () => {
|
||||
await device.launchApp({permissions: {
|
||||
photos: 'YES',
|
||||
// camera: 'YES',
|
||||
}});
|
||||
});
|
||||
|
||||
it('should load example app with no errors and show all the examples by default', async () => {
|
||||
await expect(element(by.text('Big Images'))).toExist();
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* 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 strict-local
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
const ReactNative = require('react-native');
|
||||
const {Image, StyleSheet, View, ScrollView} = ReactNative;
|
||||
|
||||
import type {PhotoIdentifier} from 'CameraRoll';
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
asset: PhotoIdentifier,
|
||||
|}>;
|
||||
|
||||
type State = {|
|
||||
asset: PhotoIdentifier,
|
||||
|};
|
||||
|
||||
class AssetScaledImageExample extends React.Component<Props, State> {
|
||||
state = {
|
||||
asset: this.props.asset,
|
||||
};
|
||||
|
||||
render() {
|
||||
const image = this.state.asset.node.image;
|
||||
return (
|
||||
<ScrollView>
|
||||
<View style={styles.row}>
|
||||
<Image source={image} style={styles.imageWide} />
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<Image source={image} style={styles.imageThumb} />
|
||||
<Image source={image} style={styles.imageThumb} />
|
||||
<Image source={image} style={styles.imageThumb} />
|
||||
</View>
|
||||
<View style={styles.row}>
|
||||
<Image source={image} style={styles.imageT1} />
|
||||
<Image source={image} style={styles.imageT2} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
padding: 5,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
imageWide: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
width: 320,
|
||||
height: 240,
|
||||
margin: 5,
|
||||
},
|
||||
imageThumb: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
width: 100,
|
||||
height: 100,
|
||||
margin: 5,
|
||||
},
|
||||
imageT1: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
width: 212,
|
||||
height: 320,
|
||||
margin: 5,
|
||||
},
|
||||
imageT2: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
width: 100,
|
||||
height: 320,
|
||||
margin: 5,
|
||||
},
|
||||
});
|
||||
|
||||
exports.title = '<AssetScaledImageExample>';
|
||||
exports.description =
|
||||
'Example component that displays the automatic scaling capabilities of the <Image /> tag';
|
||||
module.exports = AssetScaledImageExample;
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
const ReactNative = require('react-native');
|
||||
const {
|
||||
CameraRoll,
|
||||
Image,
|
||||
Slider,
|
||||
StyleSheet,
|
||||
Switch,
|
||||
Text,
|
||||
View,
|
||||
TouchableOpacity,
|
||||
} = ReactNative;
|
||||
|
||||
const invariant = require('invariant');
|
||||
|
||||
const CameraRollView = require('./CameraRollView');
|
||||
|
||||
const AssetScaledImageExampleView = require('./AssetScaledImageExample');
|
||||
|
||||
import type {PhotoIdentifier, GroupTypes} from 'CameraRoll';
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
navigator?: ?Array<
|
||||
$ReadOnly<{|
|
||||
title: string,
|
||||
component: Class<React.Component<any, any>>,
|
||||
backButtonTitle: string,
|
||||
passProps: $ReadOnly<{|asset: PhotoIdentifier|}>,
|
||||
|}>,
|
||||
>,
|
||||
|}>;
|
||||
|
||||
type State = {|
|
||||
groupTypes: GroupTypes,
|
||||
sliderValue: number,
|
||||
bigImages: boolean,
|
||||
|};
|
||||
|
||||
export default class CameraRollExample extends React.Component<Props, State> {
|
||||
state = {
|
||||
groupTypes: 'SavedPhotos',
|
||||
sliderValue: 1,
|
||||
bigImages: true,
|
||||
};
|
||||
_cameraRollView: ?React.ElementRef<typeof CameraRollView>;
|
||||
render() {
|
||||
return (
|
||||
<View>
|
||||
<Switch
|
||||
onValueChange={this._onSwitchChange}
|
||||
value={this.state.bigImages}
|
||||
/>
|
||||
<Text>{(this.state.bigImages ? 'Big' : 'Small') + ' Images'}</Text>
|
||||
<Slider
|
||||
value={this.state.sliderValue}
|
||||
onValueChange={this._onSliderChange}
|
||||
/>
|
||||
<Text>{'Group Type: ' + this.state.groupTypes}</Text>
|
||||
<CameraRollView
|
||||
ref={ref => {
|
||||
this._cameraRollView = ref;
|
||||
}}
|
||||
batchSize={20}
|
||||
groupTypes={this.state.groupTypes}
|
||||
renderImage={this._renderImage}
|
||||
bigImages={this.state.bigImages}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
loadAsset(asset) {
|
||||
if (this.props.navigator) {
|
||||
this.props.navigator.push({
|
||||
title: 'Camera Roll Image',
|
||||
component: AssetScaledImageExampleView,
|
||||
backButtonTitle: 'Back',
|
||||
passProps: {asset: asset},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_renderImage = (asset: PhotoIdentifier) => {
|
||||
const imageSize = this.state.bigImages ? 150 : 75;
|
||||
const imageStyle = [styles.image, {width: imageSize, height: imageSize}];
|
||||
const {location} = asset.node;
|
||||
const locationStr = location
|
||||
? JSON.stringify(location)
|
||||
: 'Unknown location';
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={asset.node.image.uri}
|
||||
onPress={this.loadAsset.bind(this, asset)}>
|
||||
<View style={styles.row}>
|
||||
<Image source={asset.node.image} style={imageStyle} />
|
||||
<View style={styles.info}>
|
||||
<Text style={styles.url}>{asset.node.image.uri}</Text>
|
||||
<Text>{locationStr}</Text>
|
||||
<Text>{asset.node.group_name}</Text>
|
||||
<Text>{new Date(asset.node.timestamp).toString()}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
_onSliderChange = value => {
|
||||
const options = Object.keys(CameraRoll.GroupTypesOptions);
|
||||
const index = Math.floor(value * options.length * 0.99);
|
||||
const groupTypes = options[index];
|
||||
if (groupTypes !== this.state.groupTypes) {
|
||||
this.setState({groupTypes: groupTypes});
|
||||
}
|
||||
};
|
||||
|
||||
_onSwitchChange = value => {
|
||||
invariant(this._cameraRollView, 'ref should be set');
|
||||
this.setState({bigImages: value});
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
},
|
||||
url: {
|
||||
fontSize: 9,
|
||||
marginBottom: 14,
|
||||
},
|
||||
image: {
|
||||
margin: 4,
|
||||
},
|
||||
info: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
|
@ -0,0 +1,265 @@
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
const ReactNative = require('react-native');
|
||||
const {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
CameraRoll,
|
||||
Image,
|
||||
FlatList,
|
||||
PermissionsAndroid,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
} = ReactNative;
|
||||
|
||||
const groupByEveryN = require('groupByEveryN');
|
||||
const logError = require('logError');
|
||||
|
||||
import type {
|
||||
PhotoIdentifier,
|
||||
PhotoIdentifiersPage,
|
||||
GetPhotosParams,
|
||||
} from 'CameraRoll';
|
||||
|
||||
type Props = $ReadOnly<{|
|
||||
/**
|
||||
* The group where the photos will be fetched from. Possible
|
||||
* values are 'Album', 'All', 'Event', 'Faces', 'Library', 'PhotoStream'
|
||||
* and SavedPhotos.
|
||||
*/
|
||||
groupTypes:
|
||||
| 'Album'
|
||||
| 'All'
|
||||
| 'Event'
|
||||
| 'Faces'
|
||||
| 'Library'
|
||||
| 'PhotoStream'
|
||||
| 'SavedPhotos',
|
||||
|
||||
/**
|
||||
* Number of images that will be fetched in one page.
|
||||
*/
|
||||
batchSize: number,
|
||||
|
||||
/**
|
||||
* A function that takes a single image as a parameter and renders it.
|
||||
*/
|
||||
renderImage: PhotoIdentifier => React.Node,
|
||||
|
||||
/**
|
||||
* imagesPerRow: Number of images to be shown in each row.
|
||||
*/
|
||||
imagesPerRow: number,
|
||||
|
||||
/**
|
||||
* A boolean that indicates if we should render large or small images.
|
||||
*/
|
||||
bigImages?: boolean,
|
||||
|
||||
/**
|
||||
* The asset type, one of 'Photos', 'Videos' or 'All'
|
||||
*/
|
||||
assetType: 'Photos' | 'Videos' | 'All',
|
||||
|}>;
|
||||
|
||||
type State = {|
|
||||
assets: Array<PhotoIdentifier>,
|
||||
data: Array<Array<?PhotoIdentifier>>,
|
||||
seen: Set<string>,
|
||||
lastCursor: ?string,
|
||||
noMore: boolean,
|
||||
loadingMore: boolean,
|
||||
|};
|
||||
|
||||
type Row = {
|
||||
item: Array<?PhotoIdentifier>,
|
||||
};
|
||||
|
||||
class CameraRollView extends React.Component<Props, State> {
|
||||
static defaultProps = {
|
||||
groupTypes: 'SavedPhotos',
|
||||
batchSize: 5,
|
||||
imagesPerRow: 1,
|
||||
assetType: 'Photos',
|
||||
renderImage: function(asset: PhotoIdentifier) {
|
||||
const imageSize = 150;
|
||||
const imageStyle = [styles.image, {width: imageSize, height: imageSize}];
|
||||
return <Image source={asset.node.image} style={imageStyle} />;
|
||||
},
|
||||
};
|
||||
|
||||
state = this.getInitialState();
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
assets: [],
|
||||
data: [],
|
||||
seen: new Set(),
|
||||
lastCursor: null,
|
||||
noMore: false,
|
||||
loadingMore: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
||||
if (this.props.groupTypes !== nextProps.groupTypes) {
|
||||
this.fetch(true);
|
||||
}
|
||||
}
|
||||
|
||||
async _fetch(clear?: boolean) {
|
||||
if (clear) {
|
||||
this.setState(this.getInitialState(), this.fetch);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
const result = await PermissionsAndroid.request(
|
||||
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
|
||||
{
|
||||
title: 'Permission Explanation',
|
||||
message: 'RNTester would like to access your pictures.',
|
||||
},
|
||||
);
|
||||
if (result !== 'granted') {
|
||||
Alert.alert('Access to pictures was denied.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const fetchParams: GetPhotosParams = {
|
||||
first: this.props.batchSize,
|
||||
groupTypes: this.props.groupTypes,
|
||||
assetType: this.props.assetType,
|
||||
};
|
||||
if (Platform.OS === 'android') {
|
||||
// not supported in android
|
||||
delete fetchParams.groupTypes;
|
||||
}
|
||||
|
||||
if (this.state.lastCursor) {
|
||||
fetchParams.after = this.state.lastCursor;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await CameraRoll.getPhotos(fetchParams);
|
||||
this._appendAssets(data);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches more images from the camera roll. If clear is set to true, it will
|
||||
* set the component to its initial state and re-fetch the images.
|
||||
*/
|
||||
fetch = (clear?: boolean) => {
|
||||
if (!this.state.loadingMore) {
|
||||
this.setState({loadingMore: true}, () => {
|
||||
this._fetch(clear);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FlatList
|
||||
keyExtractor={(_, idx) => String(idx)}
|
||||
renderItem={this._renderItem}
|
||||
ListFooterComponent={this._renderFooterSpinner}
|
||||
onEndReached={this._onEndReached}
|
||||
onEndReachedThreshold={0.2}
|
||||
style={styles.container}
|
||||
data={this.state.data || []}
|
||||
extraData={this.props.bigImages + this.state.noMore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_renderFooterSpinner = () => {
|
||||
if (!this.state.noMore) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
_renderItem = (row: Row) => {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
{row.item.map(image => (image ? this.props.renderImage(image) : null))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
_appendAssets(data: PhotoIdentifiersPage) {
|
||||
const assets = data.edges;
|
||||
const newState: $Shape<State> = {loadingMore: false};
|
||||
|
||||
if (!data.page_info.has_next_page) {
|
||||
newState.noMore = true;
|
||||
}
|
||||
|
||||
if (assets.length > 0) {
|
||||
newState.lastCursor = data.page_info.end_cursor;
|
||||
newState.seen = new Set(this.state.seen);
|
||||
|
||||
// Unique assets efficiently
|
||||
// Checks new pages against seen objects
|
||||
const uniqAssets = [];
|
||||
for (let index = 0; index < assets.length; index++) {
|
||||
const asset = assets[index];
|
||||
let value = asset.node.image.uri;
|
||||
if (newState.seen.has(value)) {
|
||||
continue;
|
||||
}
|
||||
newState.seen.add(value);
|
||||
uniqAssets.push(asset);
|
||||
}
|
||||
|
||||
newState.assets = this.state.assets.concat(uniqAssets);
|
||||
newState.data = groupByEveryN<PhotoIdentifier>(
|
||||
newState.assets,
|
||||
this.props.imagesPerRow,
|
||||
);
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
_onEndReached = () => {
|
||||
if (!this.state.noMore) {
|
||||
this.fetch();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
},
|
||||
image: {
|
||||
margin: 4,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = CameraRollView;
|
Loading…
Reference in New Issue