1import React from 'react'; 2import * as MediaLibrary from 'expo-media-library'; 3import * as Permissions from 'expo-permissions'; 4import { 5 ActivityIndicator, 6 Button as RNButton, 7 Dimensions, 8 View, 9 Text, 10 StyleSheet, 11 FlatList, 12 RefreshControl, 13 ListRenderItem, 14} from 'react-native'; 15import { NavigationEvents, NavigationScreenProps, NavigationScreenConfig } from 'react-navigation'; 16 17import Colors from '../../constants/Colors'; 18import MediaLibraryCell from './MediaLibraryCell'; 19import Button from '../../components/Button'; 20import HeadingText from '../../components/HeadingText'; 21 22const COLUMNS = 3; 23const PAGE_SIZE = COLUMNS * 10; 24const WINDOW_SIZE = Dimensions.get('window'); 25 26const mediaTypeStates: { [key in MediaLibrary.MediaTypeValue]: MediaLibrary.MediaTypeValue } = { 27 [MediaLibrary.MediaType.unknown]: MediaLibrary.MediaType.photo, 28 [MediaLibrary.MediaType.photo]: MediaLibrary.MediaType.video, 29 [MediaLibrary.MediaType.video]: MediaLibrary.MediaType.audio, 30 [MediaLibrary.MediaType.audio]: MediaLibrary.MediaType.unknown, 31}; 32 33const sortByStates: { [key in MediaLibrary.SortByKey]: MediaLibrary.SortByKey } = { 34 [MediaLibrary.SortBy.default]: MediaLibrary.SortBy.creationTime, 35 [MediaLibrary.SortBy.creationTime]: MediaLibrary.SortBy.modificationTime, 36 [MediaLibrary.SortBy.modificationTime]: MediaLibrary.SortBy.mediaType, 37 [MediaLibrary.SortBy.mediaType]: MediaLibrary.SortBy.width, 38 [MediaLibrary.SortBy.width]: MediaLibrary.SortBy.height, 39 [MediaLibrary.SortBy.height]: MediaLibrary.SortBy.duration, 40 [MediaLibrary.SortBy.duration]: MediaLibrary.SortBy.default, 41}; 42 43interface State { 44 assets: MediaLibrary.Asset[]; 45 endCursor?: string; 46 hasNextPage?: boolean; 47 permission?: Permissions.PermissionStatus; 48 refreshing: boolean; 49 mediaType: MediaLibrary.MediaTypeValue; 50 sortBy: MediaLibrary.SortByKey; 51} 52 53export default class MediaLibraryScreen extends React.Component<NavigationScreenProps, State> { 54 static navigationOptions: NavigationScreenConfig<{}> = ({ navigation }) => { 55 const goToAlbums = () => navigation.navigate('MediaAlbums'); 56 const clearAlbumSelection = () => navigation.setParams({ album: null }); 57 const { params } = navigation.state; 58 const isAlbumSet = params && params.album; 59 60 return { 61 title: 'MediaLibrary', 62 headerRight: ( 63 <View style={{ marginRight: 5 }}> 64 <RNButton 65 title={isAlbumSet ? 'Show all' : 'Albums'} 66 onPress={isAlbumSet ? clearAlbumSelection : goToAlbums} 67 color={Colors.tintColor} 68 /> 69 </View> 70 ), 71 }; 72 } 73 74 readonly state: State = { 75 assets: [], 76 refreshing: true, 77 mediaType: MediaLibrary.MediaType.photo, 78 sortBy: MediaLibrary.SortBy.default, 79 }; 80 81 isLoadingAssets = false; 82 83 libraryChangeSubscription?: { remove: () => void }; 84 85 componentDidFocus = async () => { 86 const { status } = await Permissions.askAsync(Permissions.CAMERA_ROLL); 87 this.setState({ permission: status, assets: [], endCursor: undefined, hasNextPage: undefined }); 88 this.loadMoreAssets(); 89 90 if (this.libraryChangeSubscription) { 91 this.libraryChangeSubscription.remove(); 92 } 93 this.libraryChangeSubscription = MediaLibrary.addListener(() => { 94 this.loadMoreAssets([]); 95 }); 96 } 97 98 componentWillUnmount() { 99 this.libraryChangeSubscription!.remove(); 100 this.libraryChangeSubscription = undefined; 101 } 102 103 getAlbum() { 104 const { params } = this.props.navigation.state; 105 return params && params.album; 106 } 107 108 async loadMoreAssets(currentAssets = this.state.assets, cursor = this.state.endCursor) { 109 if ( 110 this.isLoadingAssets || 111 (cursor === this.state.endCursor && this.state.hasNextPage === false) 112 ) { 113 return; 114 } 115 116 const { state } = this; 117 const album = this.getAlbum(); 118 119 this.isLoadingAssets = true; 120 121 const { assets, endCursor, hasNextPage } = await MediaLibrary.getAssetsAsync({ 122 first: PAGE_SIZE, 123 after: cursor, 124 mediaType: state.mediaType, 125 sortBy: state.sortBy, 126 album: album && album.id, 127 }); 128 129 const lastAsset = currentAssets[currentAssets.length - 1]; 130 131 if (!lastAsset || lastAsset.id === cursor) { 132 this.setState({ 133 assets: ([] as MediaLibrary.Asset[]).concat(currentAssets, assets), 134 endCursor, 135 hasNextPage, 136 refreshing: false, 137 }); 138 } 139 140 this.isLoadingAssets = false; 141 } 142 143 refresh = (refreshingFlag = true) => { 144 this.setState( 145 { assets: [], endCursor: undefined, hasNextPage: undefined, refreshing: refreshingFlag }, 146 () => { 147 this.loadMoreAssets(); 148 } 149 ); 150 } 151 152 toggleMediaType = () => { 153 const mediaType = mediaTypeStates[this.state.mediaType]; 154 this.setState({ mediaType }); 155 this.refresh(false); 156 } 157 158 toggleSortBy = () => { 159 const sortBy = sortByStates[this.state.sortBy]; 160 this.setState({ sortBy }); 161 this.refresh(false); 162 } 163 164 keyExtractor = (item: MediaLibrary.Asset) => item.id; 165 166 onEndReached = () => { 167 this.loadMoreAssets(); 168 } 169 170 onCellPress = (asset: MediaLibrary.Asset) => { 171 this.props.navigation.navigate('MediaDetails', { 172 asset, 173 album: this.getAlbum(), 174 onGoBack: this.refresh, 175 }); 176 } 177 178 renderRowItem: ListRenderItem<MediaLibrary.Asset> = ({ item }) => { 179 return ( 180 <MediaLibraryCell 181 style={{ width: WINDOW_SIZE.width / COLUMNS }} 182 asset={item} 183 onPress={this.onCellPress} 184 /> 185 ); 186 } 187 188 renderHeader = () => { 189 const album = this.getAlbum(); 190 191 return ( 192 <View style={styles.header}> 193 <HeadingText style={styles.headerText}> 194 {album ? `Album: ${album.title}` : 'All albums'} 195 </HeadingText> 196 197 <View style={styles.headerButtons}> 198 <Button 199 style={styles.button} 200 title={`Media type: ${this.state.mediaType}`} 201 onPress={this.toggleMediaType} 202 /> 203 <Button 204 style={styles.button} 205 title={`Sort by key: ${this.state.sortBy}`} 206 onPress={this.toggleSortBy} 207 /> 208 </View> 209 </View> 210 ); 211 } 212 213 renderFooter = () => { 214 const { assets, refreshing, mediaType } = this.state; 215 216 if (refreshing) { 217 return ( 218 <View style={styles.footer}> 219 <ActivityIndicator animating /> 220 </View> 221 ); 222 } 223 if (assets.length === 0) { 224 return ( 225 <View style={styles.noAssets}> 226 <Text>{`You don't have any assets with type: ${mediaType}`}</Text> 227 </View> 228 ); 229 } 230 return null; 231 } 232 233 renderContent() { 234 const { assets, permission, refreshing } = this.state; 235 236 if (!permission) { 237 return null; 238 } 239 if (permission !== 'granted') { 240 return ( 241 <View style={styles.permissions}> 242 <Text> 243 Missing CAMERA_ROLL permission. To continue, you'll need to allow media gallery access 244 in Settings. 245 </Text> 246 </View> 247 ); 248 } 249 250 return ( 251 <FlatList 252 contentContainerStyle={styles.flatList} 253 data={assets} 254 numColumns={COLUMNS} 255 keyExtractor={this.keyExtractor} 256 onEndReachedThreshold={0.5} 257 onEndReached={this.onEndReached} 258 renderItem={this.renderRowItem} 259 ListHeaderComponent={this.renderHeader} 260 ListFooterComponent={this.renderFooter} 261 refreshControl={<RefreshControl refreshing={refreshing} onRefresh={this.refresh} />} 262 /> 263 ); 264 } 265 266 render() { 267 return ( 268 <View style={styles.mediaGallery}> 269 <NavigationEvents onDidFocus={this.componentDidFocus} /> 270 {this.renderContent()} 271 </View> 272 ); 273 } 274} 275 276const styles = StyleSheet.create({ 277 mediaGallery: { 278 flex: 1, 279 }, 280 flatList: { 281 marginHorizontal: 1, 282 }, 283 permissions: { 284 flex: 1, 285 justifyContent: 'center', 286 alignItems: 'center', 287 }, 288 button: { 289 marginHorizontal: 5, 290 }, 291 header: { 292 paddingTop: 0, 293 paddingBottom: 16, 294 paddingHorizontal: 10, 295 }, 296 headerText: { 297 alignSelf: 'center', 298 }, 299 headerButtons: { 300 marginTop: 5, 301 paddingVertical: 10, 302 flexDirection: 'row', 303 justifyContent: 'center', 304 alignItems: 'center', 305 }, 306 footer: { 307 padding: 10, 308 }, 309 noAssets: { 310 paddingVertical: 20, 311 justifyContent: 'center', 312 alignItems: 'center', 313 }, 314}); 315