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