2015-02-20 04:10:52 +00:00
|
|
|
/**
|
2015-03-23 20:35:08 +00:00
|
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
|
|
* All rights reserved.
|
|
|
|
*
|
|
|
|
* This source code is licensed under the BSD-style license found in the
|
|
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
|
|
*
|
2015-02-20 04:10:52 +00:00
|
|
|
* @flow
|
|
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
var React = require('react-native');
|
|
|
|
var {
|
|
|
|
ListView,
|
|
|
|
ScrollView,
|
|
|
|
ActivityIndicatorIOS,
|
|
|
|
StyleSheet,
|
|
|
|
Text,
|
|
|
|
TextInput,
|
|
|
|
View,
|
|
|
|
} = React;
|
2015-03-24 18:35:59 +00:00
|
|
|
var TimerMixin = require('react-timer-mixin');
|
2015-02-20 04:10:52 +00:00
|
|
|
|
|
|
|
var MovieCell = require('./MovieCell');
|
|
|
|
var MovieScreen = require('./MovieScreen');
|
|
|
|
|
|
|
|
var fetch = require('fetch');
|
|
|
|
|
|
|
|
var API_URL = 'http://api.rottentomatoes.com/api/public/v1.0/';
|
|
|
|
var API_KEYS = ['7waqfqbprs7pajbz28mqf6vz', 'y4vwv8m33hed9ety83jmv52f'];
|
|
|
|
|
|
|
|
// Results should be cached keyed by the query
|
|
|
|
// with values of null meaning "being fetched"
|
|
|
|
// and anything besides null and undefined
|
|
|
|
// as the result of a valid query
|
|
|
|
var resultsCache = {
|
|
|
|
dataForQuery: {},
|
|
|
|
nextPageNumberForQuery: {},
|
|
|
|
totalForQuery: {},
|
|
|
|
};
|
|
|
|
|
|
|
|
var LOADING = {};
|
|
|
|
|
|
|
|
var SearchScreen = React.createClass({
|
|
|
|
mixins: [TimerMixin],
|
|
|
|
|
2015-03-21 01:33:06 +00:00
|
|
|
timeoutID: (null: any),
|
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
getInitialState: function() {
|
|
|
|
return {
|
|
|
|
isLoading: false,
|
|
|
|
isLoadingTail: false,
|
2015-03-03 00:02:42 +00:00
|
|
|
dataSource: new ListView.DataSource({
|
2015-02-20 04:10:52 +00:00
|
|
|
rowHasChanged: (row1, row2) => row1 !== row2,
|
|
|
|
}),
|
|
|
|
filter: '',
|
|
|
|
queryNumber: 0,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
componentDidMount: function() {
|
|
|
|
this.searchMovies('');
|
|
|
|
},
|
|
|
|
|
|
|
|
_urlForQueryAndPage: function(query: string, pageNumber: ?number): string {
|
|
|
|
var apiKey = API_KEYS[this.state.queryNumber % API_KEYS.length];
|
|
|
|
if (query) {
|
|
|
|
return (
|
|
|
|
API_URL + 'movies.json?apikey=' + apiKey + '&q=' +
|
|
|
|
encodeURIComponent(query) + '&page_limit=20&page=' + pageNumber
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// With no query, load latest movies
|
|
|
|
return (
|
|
|
|
API_URL + 'lists/movies/in_theaters.json?apikey=' + apiKey +
|
|
|
|
'&page_limit=20&page=' + pageNumber
|
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
searchMovies: function(query: string) {
|
|
|
|
this.timeoutID = null;
|
|
|
|
|
|
|
|
this.setState({filter: query});
|
|
|
|
|
|
|
|
var cachedResultsForQuery = resultsCache.dataForQuery[query];
|
|
|
|
if (cachedResultsForQuery) {
|
|
|
|
if (!LOADING[query]) {
|
|
|
|
this.setState({
|
|
|
|
dataSource: this.getDataSource(cachedResultsForQuery),
|
|
|
|
isLoading: false
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.setState({isLoading: true});
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
LOADING[query] = true;
|
|
|
|
resultsCache.dataForQuery[query] = null;
|
|
|
|
this.setState({
|
|
|
|
isLoading: true,
|
|
|
|
queryNumber: this.state.queryNumber + 1,
|
|
|
|
isLoadingTail: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
fetch(this._urlForQueryAndPage(query, 1))
|
|
|
|
.then((response) => response.json())
|
2015-03-13 00:06:18 +00:00
|
|
|
.catch((error) => {
|
|
|
|
LOADING[query] = false;
|
|
|
|
resultsCache.dataForQuery[query] = undefined;
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
dataSource: this.getDataSource([]),
|
|
|
|
isLoading: false,
|
|
|
|
});
|
|
|
|
})
|
2015-02-20 04:10:52 +00:00
|
|
|
.then((responseData) => {
|
|
|
|
LOADING[query] = false;
|
|
|
|
resultsCache.totalForQuery[query] = responseData.total;
|
|
|
|
resultsCache.dataForQuery[query] = responseData.movies;
|
|
|
|
resultsCache.nextPageNumberForQuery[query] = 2;
|
|
|
|
|
|
|
|
if (this.state.filter !== query) {
|
|
|
|
// do not update state if the query is stale
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
isLoading: false,
|
|
|
|
dataSource: this.getDataSource(responseData.movies),
|
|
|
|
});
|
|
|
|
})
|
2015-03-13 00:06:18 +00:00
|
|
|
.done();
|
2015-02-20 04:10:52 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
hasMore: function(): boolean {
|
|
|
|
var query = this.state.filter;
|
|
|
|
if (!resultsCache.dataForQuery[query]) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
resultsCache.totalForQuery[query] !==
|
|
|
|
resultsCache.dataForQuery[query].length
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
onEndReached: function() {
|
|
|
|
var query = this.state.filter;
|
|
|
|
if (!this.hasMore() || this.state.isLoadingTail) {
|
|
|
|
// We're already fetching or have all the elements so noop
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (LOADING[query]) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
LOADING[query] = true;
|
|
|
|
this.setState({
|
|
|
|
queryNumber: this.state.queryNumber + 1,
|
|
|
|
isLoadingTail: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
var page = resultsCache.nextPageNumberForQuery[query];
|
|
|
|
fetch(this._urlForQueryAndPage(query, page))
|
|
|
|
.then((response) => response.json())
|
2015-03-13 00:06:18 +00:00
|
|
|
.catch((error) => {
|
|
|
|
console.error(error);
|
|
|
|
LOADING[query] = false;
|
|
|
|
this.setState({
|
|
|
|
isLoadingTail: false,
|
|
|
|
});
|
|
|
|
})
|
2015-02-20 04:10:52 +00:00
|
|
|
.then((responseData) => {
|
|
|
|
var moviesForQuery = resultsCache.dataForQuery[query].slice();
|
|
|
|
|
|
|
|
LOADING[query] = false;
|
|
|
|
// We reached the end of the list before the expected number of results
|
|
|
|
if (!responseData.movies) {
|
|
|
|
resultsCache.totalForQuery[query] = moviesForQuery.length;
|
|
|
|
} else {
|
|
|
|
for (var i in responseData.movies) {
|
|
|
|
moviesForQuery.push(responseData.movies[i]);
|
|
|
|
}
|
|
|
|
resultsCache.dataForQuery[query] = moviesForQuery;
|
|
|
|
resultsCache.nextPageNumberForQuery[query] += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.state.filter !== query) {
|
|
|
|
// do not update state if the query is stale
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
isLoadingTail: false,
|
|
|
|
dataSource: this.getDataSource(resultsCache.dataForQuery[query]),
|
|
|
|
});
|
|
|
|
})
|
2015-03-13 00:06:18 +00:00
|
|
|
.done();
|
2015-02-20 04:10:52 +00:00
|
|
|
},
|
|
|
|
|
2015-03-03 00:02:42 +00:00
|
|
|
getDataSource: function(movies: Array<any>): ListView.DataSource {
|
2015-02-20 04:10:52 +00:00
|
|
|
return this.state.dataSource.cloneWithRows(movies);
|
|
|
|
},
|
|
|
|
|
|
|
|
selectMovie: function(movie: Object) {
|
|
|
|
this.props.navigator.push({
|
|
|
|
title: movie.title,
|
|
|
|
component: MovieScreen,
|
|
|
|
passProps: {movie},
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
onSearchChange: function(event: Object) {
|
|
|
|
var filter = event.nativeEvent.text.toLowerCase();
|
|
|
|
|
|
|
|
this.clearTimeout(this.timeoutID);
|
|
|
|
this.timeoutID = this.setTimeout(() => this.searchMovies(filter), 100);
|
|
|
|
},
|
|
|
|
|
|
|
|
renderFooter: function() {
|
|
|
|
if (!this.hasMore() || !this.state.isLoadingTail) {
|
|
|
|
return <View style={styles.scrollSpinner} />;
|
|
|
|
}
|
|
|
|
return <ActivityIndicatorIOS style={styles.scrollSpinner} />;
|
|
|
|
},
|
|
|
|
|
|
|
|
renderRow: function(movie: Object) {
|
|
|
|
return (
|
|
|
|
<MovieCell
|
|
|
|
onSelect={() => this.selectMovie(movie)}
|
|
|
|
movie={movie}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
render: function() {
|
|
|
|
var content = this.state.dataSource.getRowCount() === 0 ?
|
|
|
|
<NoMovies
|
|
|
|
filter={this.state.filter}
|
|
|
|
isLoading={this.state.isLoading}
|
|
|
|
/> :
|
|
|
|
<ListView
|
|
|
|
ref="listview"
|
|
|
|
dataSource={this.state.dataSource}
|
|
|
|
renderFooter={this.renderFooter}
|
|
|
|
renderRow={this.renderRow}
|
|
|
|
onEndReached={this.onEndReached}
|
|
|
|
automaticallyAdjustContentInsets={false}
|
2015-03-03 18:22:29 +00:00
|
|
|
keyboardDismissMode="onDrag"
|
2015-02-20 04:10:52 +00:00
|
|
|
keyboardShouldPersistTaps={true}
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
/>;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<View style={styles.container}>
|
|
|
|
<SearchBar
|
|
|
|
onSearchChange={this.onSearchChange}
|
|
|
|
isLoading={this.state.isLoading}
|
|
|
|
onFocus={() => this.refs.listview.getScrollResponder().scrollTo(0, 0)}
|
|
|
|
/>
|
|
|
|
<View style={styles.separator} />
|
|
|
|
{content}
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
var NoMovies = React.createClass({
|
|
|
|
render: function() {
|
|
|
|
var text = '';
|
|
|
|
if (this.props.filter) {
|
|
|
|
text = `No results for “${this.props.filter}”`;
|
|
|
|
} else if (!this.props.isLoading) {
|
|
|
|
// If we're looking at the latest movies, aren't currently loading, and
|
|
|
|
// still have no results, show a message
|
|
|
|
text = 'No movies found';
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<View style={[styles.container, styles.centerText]}>
|
|
|
|
<Text style={styles.noMoviesText}>{text}</Text>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
var SearchBar = React.createClass({
|
|
|
|
render: function() {
|
|
|
|
return (
|
|
|
|
<View style={styles.searchBar}>
|
|
|
|
<TextInput
|
2015-03-04 22:04:52 +00:00
|
|
|
autoCapitalize="none"
|
2015-02-20 04:10:52 +00:00
|
|
|
autoCorrect={false}
|
|
|
|
onChange={this.props.onSearchChange}
|
|
|
|
placeholder="Search a movie..."
|
|
|
|
onFocus={this.props.onFocus}
|
|
|
|
style={styles.searchBarInput}
|
|
|
|
/>
|
|
|
|
<ActivityIndicatorIOS
|
|
|
|
animating={this.props.isLoading}
|
|
|
|
style={styles.spinner}
|
|
|
|
/>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
var styles = StyleSheet.create({
|
|
|
|
container: {
|
|
|
|
flex: 1,
|
|
|
|
backgroundColor: 'white',
|
|
|
|
},
|
|
|
|
centerText: {
|
|
|
|
alignItems: 'center',
|
|
|
|
},
|
|
|
|
noMoviesText: {
|
|
|
|
marginTop: 80,
|
|
|
|
color: '#888888',
|
|
|
|
},
|
|
|
|
searchBar: {
|
|
|
|
marginTop: 64,
|
|
|
|
padding: 3,
|
|
|
|
paddingLeft: 8,
|
|
|
|
flexDirection: 'row',
|
|
|
|
alignItems: 'center',
|
|
|
|
},
|
|
|
|
searchBarInput: {
|
|
|
|
fontSize: 15,
|
|
|
|
flex: 1,
|
|
|
|
height: 30,
|
|
|
|
},
|
|
|
|
separator: {
|
|
|
|
height: 1,
|
|
|
|
backgroundColor: '#eeeeee',
|
|
|
|
},
|
|
|
|
spinner: {
|
|
|
|
width: 30,
|
|
|
|
},
|
|
|
|
scrollSpinner: {
|
|
|
|
marginVertical: 20,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
module.exports = SearchScreen;
|