Skip to content
This repository was archived by the owner on Feb 27, 2022. It is now read-only.

Commit ccc4491

Browse files
author
Elad Gil
committed
better test app
1 parent 0aa4dab commit ccc4491

File tree

7 files changed

+444
-85
lines changed

7 files changed

+444
-85
lines changed

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,14 @@
4949
"@react-native-community/eslint-config": "^0.0.3",
5050
"babel-jest": "^24.5.0",
5151
"eslint": "^5.16.0",
52+
"immer": "^3.2.0",
5253
"jest": "^24.5.0",
5354
"metro-react-native-babel-preset": "^0.53.1",
54-
"react-test-renderer": "16.8.3",
5555
"react": "16.8.3",
56-
"react-native": "0.59.2"
56+
"react-native": "0.59.2",
57+
"react-native-fs": "^2.14.1",
58+
"react-native-vector-icons": "^6.6.0",
59+
"react-test-renderer": "16.8.3"
5760
},
5861
"jest": {
5962
"preset": "react-native",

testApp/App.js

Lines changed: 211 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
*/
88

99
import React, { Component } from 'react';
10-
import { StyleSheet, Text, SafeAreaView, TextInput, Button } from 'react-native';
10+
import { Text, SafeAreaView, TextInput, Button, FlatList, View, AsyncStorage, TouchableOpacity, Slider } from 'react-native';
11+
import Icon from 'react-native-vector-icons/Ionicons';
12+
import RNFS from 'react-native-fs';
13+
import produce from 'immer';
1114
import RNBGD from '../index';
15+
import styles from './Style';
1216

1317
const testURL = 'https://speed.hetzner.de/100MB.bin';
1418
const urlRegex = /^(?:https?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/;
@@ -17,88 +21,212 @@ function isValid(url) {
1721
return urlRegex.test(url);
1822
}
1923

20-
const styles = StyleSheet.create({
21-
container: {
22-
flex: 1,
23-
justifyContent: 'center',
24-
alignItems: 'center',
25-
backgroundColor: '#F5FCFF',
26-
paddingHorizontal: 10
27-
},
28-
textInput: {
29-
height: 70,
30-
width: 300,
31-
borderColor: 'grey',
32-
borderWidth: 1,
33-
padding: 10
24+
export default class App extends Component {
25+
constructor(props) {
26+
super(props);
27+
this.idsToData = {};
3428
}
35-
});
3629

37-
export default class App extends Component {
38-
state = {
39-
url: '',
40-
status: 'idle',
41-
percent: 0
42-
};
43-
44-
handleClick() {
45-
if (this.state.status === 'idle') {
46-
this.downloadTask = RNBGD.download({
47-
id: 'task',
48-
url: this.state.url || testURL,
49-
destination: `${RNBGD.directories.documents}/file`
50-
}).begin(expectedBytes => {
51-
this.setState({ totalBytes: expectedBytes });
52-
})
53-
.progress(percent => {
54-
this.setState({ percent });
55-
})
56-
.done(() => {
57-
this.setState({ status: 'idle', percent: 0 });
58-
})
59-
.error(err => {
60-
console.log(err);
61-
this.setState({ status: 'idle', percent: 0 });
62-
});
63-
64-
this.setState({ status: 'downloading' });
65-
} else if (this.state.status === 'downloading') {
66-
this.downloadTask.pause();
67-
this.setState({ status: 'paused' });
68-
} else if (this.state.status === 'paused') {
69-
this.downloadTask.resume();
70-
this.setState({ status: 'downloading' });
71-
}
72-
}
73-
74-
render() {
75-
let buttonLabel = 'Download';
76-
if (this.state.status === 'downloading') {
77-
buttonLabel = 'Pause';
78-
} else if (this.state.status === 'paused') {
79-
buttonLabel = 'Resume';
80-
}
81-
82-
return (
83-
<SafeAreaView style={styles.container}>
84-
<TextInput
85-
style={styles.textInput}
86-
textContentType="none"
87-
autoCorrect={false}
88-
multiline={true}
89-
keyboardType="url"
90-
placeholder={testURL}
91-
onChangeText={(text) => { this.setState({ url: text.toLowerCase() }); }}
92-
value={this.state.url}
93-
/>
94-
<Button
95-
title={buttonLabel}
96-
onPress={this.handleClick.bind(this)}
97-
disabled={this.state.url !== '' && !isValid(this.state.url)}
98-
/>
99-
<Text>Status: {this.state.status}</Text>
100-
<Text>Downloading: {this.state.percent * 100}%</Text>
101-
</SafeAreaView>
102-
);
103-
}
30+
state = {
31+
url: '',
32+
status: 'idle',
33+
percent: 0,
34+
downloads: [],
35+
downloadsData: {},
36+
};
37+
38+
async componentDidMount() {
39+
const tasks = await RNBGD.checkForExistingDownloads();
40+
if (tasks && tasks.length) {
41+
await this.loadDownloads();
42+
const downloadsData = {};
43+
const downloads = [];
44+
for (let task of tasks) {
45+
downloads.push(task.id);
46+
downloadsData[task.id] = {
47+
url: this.idsToData[task.id].url,
48+
percent: task.percent,
49+
total: task.totalBytes,
50+
status: task.state === 'DOWNLOADING' ? 'downloading' : 'paused',
51+
task: task
52+
};
53+
this.attachToTask(task, this.idsToData[task.id].filePath);
54+
}
55+
this.setState({
56+
downloadsData,
57+
downloads
58+
});
59+
}
60+
}
61+
62+
saveDownloads() {
63+
AsyncStorage.setItem('idsToData', JSON.stringify(this.idsToData));
64+
}
65+
66+
async loadDownloads() {
67+
const mapStr = await AsyncStorage.getItem('idsToData');
68+
try {
69+
this.idsToData = JSON.parse(mapStr);
70+
} catch (e) {
71+
console.error(e);
72+
}
73+
}
74+
75+
pauseOrResume(id) {
76+
let newStatus;
77+
const download = this.state.downloadsData[id];
78+
if (download.status === 'downloading') {
79+
download.task.pause();
80+
newStatus = 'paused';
81+
} else if (download.status === 'paused') {
82+
download.task.resume();
83+
newStatus = 'downloading';
84+
} else {
85+
console.error(`Unknown status for play or pause: ${download.status}`);
86+
return;
87+
}
88+
89+
this.setState(produce(draft => {
90+
draft.downloadsData[id].status = newStatus;
91+
}));
92+
}
93+
94+
cancel(id) {
95+
const download = this.state.downloadsData[id];
96+
download.task.stop();
97+
delete this.idsToData[id];
98+
this.saveDownloads();
99+
this.setState(produce(draft => {
100+
delete draft.downloadsData[id];
101+
draft.downloads.splice(draft.downloads.indexOf(id), 1);
102+
}));
103+
}
104+
105+
renderRow({ item: downloadId }) {
106+
const download = this.state.downloadsData[downloadId];
107+
let iconName = 'ios-pause';
108+
if (download.status === 'paused') {
109+
iconName = 'ios-play';
110+
}
111+
112+
return (
113+
<View key={downloadId} style={styles.downloadItem}>
114+
<View style={{flex: 1}}>
115+
<View>
116+
<Text>{downloadId}</Text>
117+
<Text>{download.url}</Text>
118+
</View>
119+
<Slider
120+
value={download.percent}
121+
disabled
122+
/>
123+
</View>
124+
<View style={styles.buttonsContainer}>
125+
<TouchableOpacity style={styles.button} onPress={() => this.pauseOrResume(downloadId)}>
126+
<Icon name={iconName} size={26}/>
127+
</TouchableOpacity>
128+
<TouchableOpacity style={styles.button} onPress={() => this.cancel(downloadId)}>
129+
<Icon name="ios-close" size={40}/>
130+
</TouchableOpacity>
131+
</View>
132+
</View>
133+
);
134+
}
135+
136+
attachToTask(task, filePath) {
137+
task.begin(expectedBytes => {
138+
this.setState(produce(draft => {
139+
draft.downloadsData[task.id].total = expectedBytes;
140+
draft.downloadsData[task.id].status = 'downloading';
141+
}));
142+
})
143+
.progress(percent => {
144+
this.setState(produce(draft => {
145+
draft.downloadsData[task.id].percent = percent;
146+
}));
147+
})
148+
.done(async() => {
149+
try {
150+
console.log(`Finished downloading: ${task.id}, deleting it...`);
151+
await RNFS.unlink(filePath);
152+
console.log(`Deleted ${task.id}`);
153+
} catch (e) {
154+
console.error(e);
155+
}
156+
delete this.idsToData[task.id];
157+
this.saveDownloads();
158+
this.setState(produce(draft => {
159+
delete draft.downloadsData[task.id];
160+
draft.downloads.splice(draft.downloads.indexOf(task.id), 1);
161+
}));
162+
})
163+
.error(err => {
164+
console.error(`Download ${task.id} has an error: ${err}`);
165+
delete this.idsToData[task.id];
166+
this.saveDownloads();
167+
this.setState(produce(draft => {
168+
delete draft.downloadsData[task.id];
169+
draft.downloads.splice(draft.downloads.indexOf(task.id), 1);
170+
}));
171+
});
172+
}
173+
174+
addDownload() {
175+
const id = Math.random()
176+
.toString(36)
177+
.substr(2, 6);
178+
const filePath = `${RNBGD.directories.documents}/${id}`;
179+
const url = this.state.url || `${testURL}?${id}`;
180+
const task = RNBGD.download({
181+
id: id,
182+
url: url,
183+
destination: filePath,
184+
});
185+
this.attachToTask(task, filePath);
186+
this.idsToData[id] = {
187+
url,
188+
filePath
189+
};
190+
this.saveDownloads();
191+
192+
this.setState(produce(draft => {
193+
draft.downloadsData[id] = {
194+
url: url,
195+
status: 'idle',
196+
task: task
197+
};
198+
draft.downloads.push(id);
199+
draft.url = '';
200+
}));
201+
}
202+
203+
render() {
204+
return (
205+
<SafeAreaView style={styles.container}>
206+
<TextInput
207+
style={styles.textInput}
208+
textContentType="none"
209+
autoCorrect={false}
210+
multiline={true}
211+
keyboardType="url"
212+
placeholder={testURL}
213+
onChangeText={(text) => {
214+
this.setState({ url: text.toLowerCase() });
215+
}}
216+
value={this.state.url}
217+
/>
218+
<Button
219+
title="Add Download"
220+
onPress={this.addDownload.bind(this)}
221+
disabled={this.state.url !== '' && !isValid(this.state.url)}
222+
/>
223+
<FlatList
224+
style={styles.downloadingList}
225+
data={this.state.downloads}
226+
renderItem={this.renderRow.bind(this)}
227+
extraData={this.state.downloadsData}
228+
/>
229+
</SafeAreaView>
230+
);
231+
}
104232
}

testApp/Style.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { StyleSheet } from 'react-native';
2+
3+
export default StyleSheet.create({
4+
container: {
5+
flex: 1,
6+
justifyContent: 'center',
7+
alignItems: 'center',
8+
backgroundColor: '#F5FCFF',
9+
paddingHorizontal: 10,
10+
},
11+
textInput: {
12+
height: 70,
13+
width: 300,
14+
borderColor: 'grey',
15+
borderWidth: 1,
16+
padding: 10,
17+
},
18+
downloadingList: {
19+
flex: 1,
20+
width: '100%'
21+
},
22+
downloadItem: {
23+
height: 100,
24+
flexDirection: 'row',
25+
alignItems: 'center',
26+
paddingHorizontal: 5,
27+
justifyContent: 'space-between'
28+
},
29+
buttonsContainer: {
30+
flexDirection: 'row',
31+
alignItems: 'center',
32+
justifyContent: 'space-between',
33+
},
34+
button: {
35+
alignItems: 'center',
36+
justifyContent: 'center',
37+
paddingHorizontal: 10
38+
}
39+
});

testApp/android/app/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ project.ext.react = [
7777
]
7878

7979
apply from: "../../../node_modules/react-native/react.gradle"
80+
apply from: "../../../node_modules/react-native-vector-icons/fonts.gradle"
8081

8182
/**
8283
* Set this to true to create two separate APKs instead of one:
@@ -142,6 +143,8 @@ dependencies {
142143
implementation fileTree(dir: "libs", include: ["*.jar"])
143144
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
144145
implementation "com.facebook.react:react-native:+" // From node_modules
146+
implementation project(':react-native-fs')
147+
implementation project(':react-native-vector-icons')
145148
}
146149

147150
// Run this once to be able to run the application with BUCK

testApp/android/settings.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
rootProject.name = 'testApp'
22
include ':react-native-background-downloader'
33
project(':react-native-background-downloader').projectDir = new File(rootProject.projectDir, '../../android')
4+
include ':react-native-fs'
5+
project(':react-native-fs').projectDir = new File(settingsDir, '../../node_modules/react-native-fs/android')
6+
include ':react-native-vector-icons'
7+
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../../node_modules/react-native-vector-icons/android')
48

59
include ':app'

0 commit comments

Comments
 (0)