7
7
*/
8
8
9
9
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' ;
11
14
import RNBGD from '../index' ;
15
+ import styles from './Style' ;
12
16
13
17
const testURL = 'https://speed.hetzner.de/100MB.bin' ;
14
18
const urlRegex = / ^ (?: h t t p s ? : \/ \/ ) ? [ \w . - ] + (?: \. [ \w \. - ] + ) + [ \w \- \. _ ~ : / ? # [ \] @ ! \$ & ' \( \) \* \+ , ; = . ] + $ / ;
@@ -17,88 +21,212 @@ function isValid(url) {
17
21
return urlRegex . test ( url ) ;
18
22
}
19
23
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 = { } ;
34
28
}
35
- } ) ;
36
29
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
+ }
104
232
}
0 commit comments